Activate a ZeroMail inbox
This is the runbook to hand your agent (or follow by hand). ZeroMail is receive-only — there is no send, reply, or compose endpoint. Reading, searching, and subscribing are free; only an optional nicer address costs anything, and your agent pays for it from a funded Zero wallet.
- Create or connect the inbox wallet. If the user already has a Zero wallet, use it. If not, create a fresh unfunded EVM wallet locally for this inbox.
- Discover the service descriptor.
- Sign in with a wallet signature to get a token — your default inbox is created automatically.
- Get your address and read mail.
- Optionally claim a paid vanity handle or forward an inbox you already own.
- Optionally subscribe so new mail is pushed to you.
0 · Create or connect the inbox wallet
A wallet is the account your agent uses with ZeroMail. It has a public address, such as eip155:8453:0x..., and it proves control by signing the ZeroMail challenge. ZeroMail never receives the private key and never asks for an email password.
Before any API setup, do one of these:
- Use an existing Zero wallet if the user already has one.
- Otherwise create a fresh unfunded EVM wallet locally for this inbox. The default chain id is Base:
eip155:8453. - Save the private key in the user's password manager or secret store. It is the recovery credential for this inbox.
1 · Discover
Read the machine-readable descriptor and OpenAPI. They list every capability, mark which are free, and confirm the receive-only contract.
curl -s https://api.withzero.com/.well-known/zeromail.json
curl -s https://api.withzero.com/openapi.json
Prefer plain text? https://api.withzero.com/start is this runbook in a format that pastes cleanly into an agent.
2 · Sign in with the wallet
Use the wallet from step 0. Request a nonce, sign the returned message with that wallet, then verify. ZeroMail never sees private keys — only the signature. The first successful verify provisions the default mailbox automatically.
curl -s https://api.withzero.com/auth/nonce \
-H "content-type: application/json" \
-d '{"wallet":"eip155:8453:0xabc...","rail":"evm"}'
curl -s https://api.withzero.com/auth/verify \
-H "content-type: application/json" \
-d '{
"wallet":"eip155:8453:0xabc...",
"rail":"evm",
"nonce":"NONCE",
"message":"MESSAGE_FROM_NONCE_RESPONSE",
"signature":"0x..."
}'
Verify returns access_token, refresh_token, agent_id, and wallet. Send the token on every call as Authorization: Bearer ACCESS_TOKEN; when it expires, exchange the refresh token at /auth/refresh.
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
const api = "https://api.withzero.com";
const privateKey = process.env.ZEROMAIL_PRIVATE_KEY || generatePrivateKey();
const account = privateKeyToAccount(privateKey);
const wallet = "eip155:8453:" + account.address;
const nonce = await fetch(api + "/auth/nonce", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ wallet, rail: "evm" })
}).then((r) => r.json());
const signature = await account.signMessage({ message: nonce.message });
const auth = await fetch(api + "/auth/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
wallet,
rail: "evm",
nonce: nonce.nonce,
message: nonce.message,
signature
})
}).then((r) => r.json());
// Save privateKey securely. Use auth.access_token for API calls.
3 · Get your address & read mail
The first verify already created your inbox; POST /v1/mailboxes is idempotent if you want to be explicit. The me alias always resolves to the signed-in wallet's mailbox.
curl -s https://api.withzero.com/v1/mailboxes/me \
-H "Authorization: Bearer $TOKEN"
curl -s "https://api.withzero.com/v1/mailboxes/me/messages?limit=10" \
-H "Authorization: Bearer $TOKEN"
From there: ...messages:search?q=... to search, ...messages/msg_... to fetch one, and ...messages:markRead to mark read. All free, no payment headers.
4 · Optional — claim a nicer address
The default address is free and complete. For something memorable like name@withzero.com, claim a handle. This is the paid upgrade path: fund or import the same wallet through Zero, then settle the claim over x402/MPP — no card and no separate ZeroMail account.
Check availability first (free, no auth) — reason is taken, reserved, or invalid when not available:
curl -s "https://api.withzero.com/v1/handles/check?handle=acme-receipts"
# -> {"handle":"acme-receipts","domain":"withzero.com","address":"acme-receipts@withzero.com","available":true}
curl -s https://api.withzero.com/v1/mailboxes/me/handle \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{"handle":"acme-receipts","set_primary":true}'
Paying: if the server answers 402, it includes an x402/MPP accepts challenge. Settle it from the funded wallet and retry the same request with an X-PAYMENT header. One payment covers a one-month period; re-claiming within the period is free. (In the current preview the endpoint may still answer for free; handle the 402 if present.)
5 · Optional — forward an inbox you already own
Keep an account you already use (e.g. Gmail) and forward a copy to ZeroMail. Your agent reads the forwarded mail; the original account stays yours. See the BYO tab for the exact call. Setup, end to end:
- Create a forward link — ZeroMail returns a
forward_targetaddress. - In Gmail: Settings → See all settings → Forwarding and POP/IMAP → Add a forwarding address → add that address.
- Gmail emails a confirmation code/link to the target — which is the agent's inbox. Read it back with
...messages:search?q=Forwardingand surface the code or link to the user. - Two steps, not one. Confirming the code/link only authorizes the address. The user must then return to the same Gmail settings page, select "Forward a copy of incoming mail to <address>", choose how to handle Gmail's copy (keep in Inbox / archive / delete), and Save. Forwarding does not start until this second step. This step lives inside the user's own Gmail, so it is always a human action — even ZeroMail's auto-confirm can only click the link (step 1), never toggle this. The link goes active once forwarded mail is observed.
- Optional — forward only a subset. Instead of forwarding everything, the user can create a Gmail filter (Settings → Filters and Blocked Addresses, or "create a filter" from the search bar) with the action "Forward to <address>", so only matching mail reaches ZeroMail (and the agent for processing).
6 · Optional — get pushed new mail
Subscribe a signed webhook or open an SSE stream so new mail reaches your agent the instant it lands. See the Push tab.
Errors & the receive-only guarantee
| Status | Meaning | Do |
|---|---|---|
401 | Missing or expired token | Refresh at /auth/refresh and retry. |
404 | Mailbox not provisioned, or cross-tenant access | POST /v1/mailboxes first; only touch your own mailbox. |
409 | Handle already taken | Pick another handle. |
402 | Payment required (paid upgrades only) | Settle from the wallet, retry with X-PAYMENT. |
422 | Invalid input (e.g. bad handle) | Fix the input and retry. |
501 | Reserved route (raw .eml / attachments) | Not available in the preview. |
How ZeroMail fits into an agent system
ZeroMail is an inbox your agent can read. People and services send normal email to a ZeroMail address; your agent reads the parsed messages over the API.
The receive-only inbox owned by a wallet. It collects messages and never sends them.
The email address people and services send to. The default one is created for the wallet.
One received email, parsed into headers, body text, attachment metadata, timestamps, and delivery state.
A webhook or SSE stream that tells your agent when new mail arrives.
Good uses
- Verification codes, account confirmations, reset links, receipts, and invoices.
- Newsletter monitoring, research digests, vendor updates, market alerts, and policy notices.
- Store emails for deals, coupon codes, shipping updates, back-in-stock notices, and price changes.
- Competitive monitoring across product updates, pricing emails, launch announcements, webinars, and changelogs.
Use a wallet inside the inbox flow
ZeroMail identifies agents by wallet. The wallet's public address identifies the agent, and a signature proves the agent controls it. For the free setup path, the agent may create a fresh unfunded EVM wallet locally; for paid upgrades, the user can fund or import that same wallet through Zero at zero.xyz. Base mainnet EVM wallets use CAIP-10 ids such as eip155:8453:0x....
curl -s https://api.withzero.com/auth/nonce \
-H "content-type: application/json" \
-d '{
"wallet": "eip155:8453:0xabc...",
"rail": "evm"
}'
curl -s https://api.withzero.com/auth/verify \
-H "content-type: application/json" \
-d '{
"wallet": "eip155:8453:0xabc...",
"rail": "evm",
"nonce": "NONCE",
"message": "MESSAGE_FROM_NONCE_RESPONSE",
"signature": "0x..."
}'
The verify response returns access_token, refresh_token, agent_id, and wallet.
Authorization: Bearer ACCESS_TOKEN
See the email address for this wallet
The me alias resolves to the signed-in wallet's mailbox. The first sign-in creates the default mailbox automatically.
curl -s https://api.withzero.com/v1/mailboxes/me \
-H "Authorization: Bearer $TOKEN"
{
"mailbox_id": "mbx_...",
"addresses": [
{ "address": "abc123@withzero.com", "primary": true }
],
"retention_days": 30
}
Created automatically, ready immediately, and enough for the full API.
Optional $1/month address if you want something easier to remember or share. The API stays free.
Read, search, and mark inbound mail
Use message_id for reads and deduplication. The RFC 822 Message-ID header is sender-controlled metadata.
curl -s "https://api.withzero.com/v1/mailboxes/me/messages?limit=10" \
-H "Authorization: Bearer $TOKEN"
curl -s "https://api.withzero.com/v1/mailboxes/me/messages:search?q=invoice" \
-H "Authorization: Bearer $TOKEN"
curl -s "https://api.withzero.com/v1/mailboxes/me/messages/msg_..." \
-H "Authorization: Bearer $TOKEN"
curl -s https://api.withzero.com/v1/mailboxes/me/messages:markRead \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{"message_ids":["msg_..."]}'
.eml and attachment download routes are reserved in the catalog and return 501 in the current preview.Subscribe with webhooks or SSE
Webhook subscriptions include a generated signing secret. The current SSE route emits a ready event and closes; longer-lived streaming is coming next.
curl -s https://api.withzero.com/v1/mailboxes/me/subscriptions \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{
"transport": "webhook",
"url": "https://agent.example/zeromail"
}'
curl -N https://api.withzero.com/v1/mailboxes/me/stream \
-H "Authorization: Bearer $TOKEN"
Forward mail you already receive
ZeroMail can give you a forwarding target for an existing mailbox, or DNS records for a domain you own. Either way, your agent only gets read access.
curl -s https://api.withzero.com/v1/mailboxes/me/external-links \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{
"mode": "forward",
"source_address": "you@gmail.com"
}'
curl -s https://api.withzero.com/v1/mailboxes/me/external-links \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{
"mode": "mx",
"source_address": "example.com"
}'
Discovery files
Agents can start from the service descriptor or OpenAPI document. Both are generated from the same registry the server uses.
{
"service": "zeromail",
"receive_only": true,
"endpoints_free": true,
"auth": {
"zero_wallet": "https://zero.xyz",
"local_wallet_allowed": true,
"default_wallet": "unfunded EVM wallet on eip155:8453",
"scheme": "bearer_jwt"
}
}
Capabilities
| Capability | Summary | Route |
|---|---|---|
mail.mailbox.provision@1 | Provision a wallet-owned receive-only mailbox. | POST /v1/mailboxes |
mail.handle.claim@1 | Claim a convenient ZeroMail address handle ($1/mo, paid from your wallet). | POST /v1/mailboxes/{id}/handle |
mail.external.link@1 | Link an external (BYO) mailbox via forwarding or MX delegation ($1/mo, paid from your wallet). | POST /v1/mailboxes/{id}/external-links |
mail.messages.list@1 | List messages in a mailbox. | GET /v1/mailboxes/{id}/messages |
mail.messages.get@1 | Get a message / raw .eml / attachment. | GET /v1/mailboxes/{id}/messages/{msgId} |
mail.messages.search@1 | Search a mailbox. | GET /v1/mailboxes/{id}/messages:search |
mail.subscriptions.create@1 | Create a webhook/SSE subscription. | POST /v1/mailboxes/{id}/subscriptions |
Endpoint catalog
| Method | Path | Operation | Auth | Status |
|---|---|---|---|---|
POST | /v1/mailboxes | provisionMailbox | wallet JWT | live |
GET | /v1/mailboxes/{id} | getMailbox | wallet JWT | live |
PATCH | /v1/mailboxes/{id} | updateMailbox | wallet JWT | live |
POST | /v1/mailboxes/{id}/handle | claimHandle | wallet JWT | live |
POST | /v1/mailboxes/{id}/external-links | linkExternal | wallet JWT | live |
GET | /v1/mailboxes/{id}/external-links | listExternalLinks | wallet JWT | live |
DELETE | /v1/mailboxes/{id}/external-links/{linkId} | deleteExternalLink | wallet JWT | live |
GET | /v1/mailboxes/{id}/addresses | listAddresses | wallet JWT | live |
POST | /v1/mailboxes/{id}/addresses:rotate | rotateAddress | wallet JWT | live |
GET | /v1/mailboxes/{id}/messages | listMessages | wallet JWT | live |
GET | /v1/mailboxes/{id}/messages/{msgId} | getMessage | wallet JWT | live |
POST | /v1/mailboxes/{id}/messages:batchGet | batchGetMessages | wallet JWT | live |
GET | /v1/mailboxes/{id}/messages/{msgId}/raw | getMessageRaw | wallet JWT | reserved |
GET | /v1/mailboxes/{id}/messages/{msgId}/attachments/{attId} | getAttachment | wallet JWT | reserved |
POST | /v1/mailboxes/{id}/messages:markRead | markRead | wallet JWT | live |
GET | /v1/mailboxes/{id}/messages:search | searchMessages | wallet JWT | live |
POST | /v1/mailboxes/{id}/subscriptions | createSubscription | wallet JWT | live |
GET | /v1/mailboxes/{id}/subscriptions | listSubscriptions | wallet JWT | live |
GET | /v1/mailboxes/{id}/subscriptions/{subId} | getSubscription | wallet JWT | live |
DELETE | /v1/mailboxes/{id}/subscriptions/{subId} | deleteSubscription | wallet JWT | live |
GET | /v1/mailboxes/{id}/stream | streamEvents | wallet JWT | live |
GET | /v1/handles/check | checkHandle | none | live |