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/v1

All 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.

LimitFreePro
Per-user request rate (1s window)8 / s10 / s
Monthly Ops quota1,500200,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).

POST
/v1/inbox

Create 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)
GET
/v1/inbox/:address

Long-poll one address. Query: since, count (default 1, max 50), limit (default 20, max 50), from, subject.

Any token (anon / web_session / user)
DELETE
/v1/inbox/:address

Burn the inbox (delete all stored emails + indices for the address).

Any token (anon / web_session / user)
GET
/v1/email/:id

Read one full email (text body, HTML body, attachments metadata, headers).

Any token (anon / web_session / user)
GET
/v1/domain/:domain

Long-poll every address under one of your verified custom domains. Same query params as /v1/inbox/:address.

Pro plan required
GET
/v1/me

Long-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.

ParameterRules
local_part2–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.
domainDefaults to the system default domain. Set to a verified custom domain that you own (requires a non-anon token).
ttlSeconds. 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.

POST
/v1/mailboxes

Validate / echo back an address. No state allocated. Body: { local_part?, domain? }. Returns { address, expires_at (RFC3339) }.

Any token (anon / web_session / user)
GET
/v1/inbox/:address/emails

List emails for an address with pagination. Query: offset, limit (default 20, max 50). Returns { emails, total, offset, limit }.

Any token (anon / web_session / user)
DELETE
/v1/inbox/:address/emails

Delete every email currently indexed for the address. Returns { deleted: <count> }.

Any token (anon / web_session / user)

Email content

GET
/v1/emails/:id

Get full email content — alias of /v1/email/:id.

Any token (anon / web_session / user)
GET
/v1/emails/:id/raw

Stream 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)
GET
/v1/emails/:id/attachments/:index

Download one attachment by zero-based index. Returns the original Content-Type from the email.

Any token (anon / web_session / user)
DELETE
/v1/emails/:id

Delete 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.

POST
/v1/tokens

Create 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)
GET
/v1/tokens

List 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)
DELETE
/v1/tokens/:id

Revoke 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.

POST
/v1/domains

Add a custom domain. Body: { domain }. Returns the DNS records to configure. Quota: 10 domains.

Pro plan required
GET
/v1/domains

List your custom domains with verify_status (pending / verified / failed) and verified_at.

Non-anon token (web_session or user)
DELETE
/v1/domains/:id

Remove 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

GET
/v1/config

Server config: system domains list, local-part rules, default TTL, OAuth client IDs, Turnstile flag. Cached 5min.

Public — no auth
POST
/v1/sessions/anon

Mint an anonymous token (browser-only; requires Cloudflare Turnstile when enabled). Returns { token, expires_at, kind: "anon" }.

Public — no auth
GET
/v1/ops

Current monthly Ops usage: { plan, limit, used, remaining, resets_at }.

Non-anon token (web_session or user)
GET
/v1/me/usage

7-day rolling storage bytes used and quota (Pro: 5 GiB; Free: 0).

Non-anon token (web_session or user)
GET
/v1/me/messages

Cross-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.

GET
/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)
GET
/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" }
StatusDescription
400Bad request — invalid parameters or body.
401Missing or invalid x-api-token.
403Forbidden — anon token where non-anon is required, non-Pro hitting a Pro route, account disabled, or Turnstile failed.
404Resource not found, or you do not own the custom domain / address.
409Conflict — domain already registered.
410Gone — email body was TTLed out of storage (error: email_expired).
429Rate limit (rate_limit_exceeded, Retry-After set) or Ops quota (ops_quota_exceeded, resets monthly) or concurrent-cap (Retry-After set).
503Real-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.