API Reference
Programmatically create disposable inboxes, long-poll for incoming mail, fetch parsed email bodies, manage user tokens and custom domains, and stream real-time events over SSE.
Base URL
https://api.mail.cx/v1All endpoints live under the /v1 prefix and require HTTPS. The base URL is the same for every request.
Authentication
Every request must carry an API token in the x-api-token header. The token kind determines what the caller can do:
- User token (tm_live_…) — Created from Dashboard → Tokens for SDK / automation. Subject to per-plan rate + monthly Ops quota. Free plan allows 1 user token, Pro 10.
- Web session (tm_live_…) — Minted by login / register / OAuth. 7-day TTL. Used by the browser UI; does not count against the user token quota and does not debit Ops.
- Anonymous (tm_anon_…) — Minted by POST /v1/sessions/anon (browser only when Cloudflare Turnstile is enabled). 24-hour TTL by default. Limited to system-domain mailboxes — cannot access custom domains, /me, /tokens, /domains, or admin routes.
curl https://api.mail.cx/v1/inbox \
-X POST \
-H "x-api-token: tm_live_your_token_here" \
-H "Content-Type: application/json" \
-d '{}'Quick start
1. Create a temporary inbox. With no body the server picks a random local-part on the default system domain. Returns the address and its expiry as a Unix-millisecond timestamp.
curl -X POST https://api.mail.cx/v1/inbox \
-H "x-api-token: tm_live_your_token_here" \
-H "Content-Type: application/json" \
-d '{}'
// → 201
{
"address": "ab12cd@qabq.com",
"expires_at": 1715706000000
}2. Wait for mail with a long-poll. The server holds the connection up to 25 seconds. 200 means new mail; 204 means the hold timed out — just re-issue with the cursor returned in next_since.
curl "https://api.mail.cx/v1/inbox/ab12cd@qabq.com?since=0" \
-H "x-api-token: tm_live_your_token_here"
// → 200
{
"emails": [
{ "id": "uuid", "from_email": "sender@example.com", "subject": "...", "preview_text": "...", "created_at": "2026-05-14T12:00:00Z" }
],
"next_since": "1715706012"
}3. Fetch the full email (text body, HTML body, attachments metadata) by id.
curl https://api.mail.cx/v1/email/{email_id} \
-H "x-api-token: tm_live_your_token_here"Rate limits & Ops quota
Two layers run atomically on every authenticated request: a per-user sliding-window rate limit and a monthly Ops counter. Anonymous tokens have no application-layer rate limit — IP throttling happens at the edge.
| Limit | Free | Pro |
|---|---|---|
| Per-user request rate (1s window) | 8 / s | 10 / s |
| Monthly Ops quota | 1,500 | 200,000 |
Authenticated responses include X-Ops-Limit and X-Ops-Remaining headers. A 429 with body {"error":"rate_limit_exceeded"} also sets Retry-After (seconds); a 429 with body {"error":"ops_quota_exceeded"} resets on the 1st of next month UTC.
What counts as an Op?
Only Dashboard-created user tokens (kind=user) debit Ops. Web-session tokens (UI login) and anonymous tokens never count against the monthly quota.
- POST /v1/inbox and POST /v1/mailboxes — creating an address
- GET /v1/email/:id and GET /v1/emails/:id — reading a parsed email
- GET /v1/emails/:id/raw and /attachments/:index — downloading raw EML or attachments
- Listing inbox, SSE streams, /me, /ops, /config, auth and admin endpoints are NOT counted
Endpoints
Inbox (v1, AI-friendly)
Compact resource shape designed for AI agents and SDK usage. The GET endpoints are long-polled — the server holds the connection up to 25 seconds (config wait_seconds, fixed by the server, not the client). When new mail arrives or the hold elapses, it responds. 204 = timed out with no mail; 200 = at least one matching email; 429 = concurrent-cap reached (Retry-After header included).
/v1/inboxCreate a temporary inbox (no state allocated — the SMTP gateway accepts mail for any address under a system domain regardless).
Any token (anon / web_session / user)/v1/inbox/:addressLong-poll one address. Query: since, count (default 1, max 50), limit (default 20, max 50), from, subject.
Any token (anon / web_session / user)/v1/inbox/:addressBurn the inbox (delete all stored emails + indices for the address).
Any token (anon / web_session / user)/v1/email/:idRead one full email (text body, HTML body, attachments metadata, headers).
Any token (anon / web_session / user)/v1/domain/:domainLong-poll every address under one of your verified custom domains. Same query params as /v1/inbox/:address.
Pro plan required/v1/meLong-poll across every owned address (Pro custom domains, future user-owned mailboxes). Cursor is a Redis Stream ID.
Non-anon token (web_session or user)POST /v1/inbox accepts an optional JSON body. All fields are optional.
| Parameter | Rules |
|---|---|
| local_part | 2–20 chars, lowercase a–z 0–9 . _ - only. Cannot start or end with . _ or -. Reserved names (admin, postmaster, abuse, support, noreply, root, webmaster, hostmaster, mail, ftp, www) are rejected. Omit for a random 6-char address. |
| domain | Defaults to the system default domain. Set to a verified custom domain that you own (requires a non-anon token). |
| ttl | Seconds. Defaults to mailbox.default_ttl in server config (3600s / 1h). Capped at mailbox.max_ttl when set. Note: TTL is informational on POST /v1/inbox — actual mail retention is controlled server-side (1h hot for Free / system, 7 days archive for Pro custom domains). |
Mailboxes (legacy, by address)
The original address-keyed resources, used by the web UI. Same data model as the v1 endpoints, returns paginated lists instead of long-polling.
/v1/mailboxesValidate / echo back an address. No state allocated. Body: { local_part?, domain? }. Returns { address, expires_at (RFC3339) }.
Any token (anon / web_session / user)/v1/inbox/:address/emailsList emails for an address with pagination. Query: offset, limit (default 20, max 50). Returns { emails, total, offset, limit }.
Any token (anon / web_session / user)/v1/inbox/:address/emailsDelete every email currently indexed for the address. Returns { deleted: <count> }.
Any token (anon / web_session / user)Email content
/v1/emails/:idGet full email content — alias of /v1/email/:id.
Any token (anon / web_session / user)/v1/emails/:id/rawStream the original .eml. Returns message/rfc822 with Content-Disposition attachment. 410 if the underlying object has been TTLed out of storage.
Any token (anon / web_session / user)/v1/emails/:id/attachments/:indexDownload one attachment by zero-based index. Returns the original Content-Type from the email.
Any token (anon / web_session / user)/v1/emails/:idDelete a single email and remove it from all per-address / per-domain indices. 204 No Content on success.
Any token (anon / web_session / user)API tokens
Manage user-kind tokens (kind=user). Web-session tokens (from login) are not listed and cannot be revoked through this endpoint — sign out instead. Free plan: 1 token. Pro plan: 10 tokens.
/v1/tokensCreate a new user token. Body: { name? }. Returns { id, name, token, created_at }. The plaintext token is returned only once.
Non-anon token (web_session or user)/v1/tokensList your user tokens. Plaintext values are not retrievable after creation — only id, name, prefix, created_at, last_used_at.
Non-anon token (web_session or user)/v1/tokens/:idRevoke a user token. 204 No Content on success. Cache invalidation is immediate.
Non-anon token (web_session or user)Custom domains (Pro)
Add a domain you own and receive mail on every address under it. Verification requires two DNS records: a TXT record _tempmail.<domain> with value tempmail-verify=<token>, and an MX record pointing to smtp.mail.cx. The background verifier polls every 5 minutes; pending domains time out after 72 hours.
/v1/domainsAdd a custom domain. Body: { domain }. Returns the DNS records to configure. Quota: 10 domains.
Pro plan required/v1/domainsList your custom domains with verify_status (pending / verified / failed) and verified_at.
Non-anon token (web_session or user)/v1/domains/:idRemove a custom domain. Also clears the Redis routing entry so SMTP stops accepting mail for it immediately.
Non-anon token (web_session or user)Account & usage
/v1/configServer config: system domains list, local-part rules, default TTL, OAuth client IDs, Turnstile flag. Cached 5min.
Public — no auth/v1/sessions/anonMint an anonymous token (browser-only; requires Cloudflare Turnstile when enabled). Returns { token, expires_at, kind: "anon" }.
Public — no auth/v1/opsCurrent monthly Ops usage: { plan, limit, used, remaining, resets_at }.
Non-anon token (web_session or user)/v1/me/usage7-day rolling storage bytes used and quota (Pro: 5 GiB; Free: 0).
Non-anon token (web_session or user)/v1/me/messagesCross-address paged history. Use ?cursor=<stream-id> for hot tail; default ?before=<unix-ms> reads cold PG archive.
Non-anon token (web_session or user)Real-time push (SSE)
Real-time email events use Server-Sent Events (SSE) over plain HTTP. The browser EventSource API auto-reconnects with the Last-Event-ID header, which the server uses as a Redis Stream cursor to replay anything missed during the gap. Since EventSource cannot set custom headers, the token is passed as a query string.
/v1/sse/addr?address=<addr>&token=<token>Stream events for a single address. System-domain addresses are open to any token kind; custom-domain addresses require the owning user token.
Any token (anon / web_session / user)/v1/sse/user?token=<token>Unified per-user stream across every owned address. Non-anon only.
Non-anon token (web_session or user)Each new email arrives as an SSE event. Heartbeat keepalives are sent as comment lines (:ka <id>) every 30 seconds so proxies do not drop the connection.
const es = new EventSource(
"https://api.mail.cx/v1/sse/user?token=tm_live_your_token_here"
);
es.addEventListener("email", (e) => {
const data = JSON.parse(e.data);
// { id, address, from, subject, preview }
});
// Heartbeat lines (":ka <last-id>") are surfaced as comments — no handler needed.
// On disconnect EventSource auto-reconnects and sends Last-Event-ID;
// the server replays any missed events from the Redis Stream.Errors
Errors are JSON objects with a machine-readable "error" key. The HTTP status code carries the category.
{ "error": "rate_limit_exceeded" }| Status | Description |
|---|---|
| 400 | Bad request — invalid parameters or body. |
| 401 | Missing or invalid x-api-token. |
| 403 | Forbidden — anon token where non-anon is required, non-Pro hitting a Pro route, account disabled, or Turnstile failed. |
| 404 | Resource not found, or you do not own the custom domain / address. |
| 409 | Conflict — domain already registered. |
| 410 | Gone — email body was TTLed out of storage (error: email_expired). |
| 429 | Rate limit (rate_limit_exceeded, Retry-After set) or Ops quota (ops_quota_exceeded, resets monthly) or concurrent-cap (Retry-After set). |
| 503 | Real-time push transport unavailable (NATS down) or captcha verifier offline. |
Security
- All traffic is HTTPS only. Plain-HTTP requests are upgraded by the edge.
- Tokens are stored as SHA-256 hashes; the server never sees plaintext after creation. Revocation invalidates the Redis token cache immediately.
- Custom-domain mailboxes are private to the registered owner. System-domain mailboxes are public by design — any anonymous caller who knows the address can read it. Do not use system-domain addresses for anything sensitive.