Received: by api.email; Mon, 03 Aug 2026 22:23:22 GMT

Reference

Docs

Everything an agent or a human needs to talk to api.email: REST endpoints, MCP tools, error codes, and plan limits. A machine-readable version lives at /llms.txt (terse) and /llms-full.txt (full), plus /openapi.json for codegen.

Quickstart

Get a namespace and an API key, then read mail back over curl or MCP. POST /v1/signup needs no authentication.

curl

curl -s -X POST https://api.email/v1/signup \
  -H 'content-type: application/json' -d '{"email":"[email protected]"}'
# => { "namespace": "swift-otter-42", "api_key": "ae_...", "example_address": "[email protected]", ... }

curl -s "https://api.email/v1/wait?tag=signup&timeout=30" \
  -H "Authorization: Bearer ae_..."

MCP

Point any MCP-capable agent at the hosted server — no install, no local process:

claude mcp add --transport http api-email https://api.email/mcp

Call the signup tool once to get an api_key, then pass it as an Authorization: Bearer ae_... header on the connection (per your MCP client's auth config) to use the other five tools.

Authentication

Every /v1 endpoint except POST /v1/signup requires an API key, sent as a standard bearer header (the MCP tools below follow the same rule; the Stripe webhook is authenticated separately, by signature):

Authorization: Bearer ae_...

Get a key from POST /v1/signup (or the MCP signup tool) — free, no email confirmation required — or mint an additional one with POST /v1/keys once you already have one. Keys are shown exactly once at creation time; api.email stores only a hash. A missing or invalid key returns 401 unauthorized.

REST reference

All paths below are relative to https://api.email. Every response is JSON; errors use the envelope described in Errors.

POST /v1/signup — no auth required

Create a free-plan account: a namespace and an API key. Rate limited to 10/hour per IP.

NameInRequiredDescription
emailbodyoptionalOptional; used for login and billing receipts.
curl -s -X POST https://api.email/v1/signup \
  -H 'content-type: application/json' -d '{"email":"[email protected]"}'

201
{ "account_id": "66b1...", "namespace": "swift-otter-42",
  "api_key": "ae_...", "plan": "free",
  "example_address": "[email protected]",
  "docs": "https://api.email/llms.txt",
  "hint": "Store api_key now — it is shown only once. ..." }

GET /v1/messages

List received messages, newest first.

NameInRequiredDescription
namespacequeryoptionalRestrict to one namespace slug.
tagqueryoptionalExact match on the tag (local part before @).
tag_prefixqueryoptionalPrefix match on tag; ignored if tag is set.
sincequeryoptionalOnly messages received after this: epoch ms or ISO 8601.
limitqueryoptional1-100, default 10.
offsetqueryoptionalDefault 0.
curl -s "https://api.email/v1/messages?tag=signup&limit=5" \
  -H "Authorization: Bearer ae_..."

200
{ "messages": [ { "id": "...", "tag": "signup", "subject": "Your code",
      "extracted": { "otp_codes": ["482913"], "links": [], "verification_links": [] }, ... } ],
  "count": 1, "limit": 5, "offset": 0 }

GET /v1/messages/{id}

Get one message by id.

NameInRequiredDescription
idpathrequiredMessage id, from GET /v1/messages.
headersqueryoptionalPass "true" to include the raw email headers.
curl -s https://api.email/v1/messages/66b2... -H "Authorization: Bearer ae_..."

200
{ "message": { "id": "66b2...", "subject": "Your code", "text": "Your code is 482913",
    "extracted": { "otp_codes": ["482913"], "links": [], "verification_links": [] },
    "attachments": [], ... } }

DELETE /v1/messages/{id}

Delete a message.

NameInRequiredDescription
idpathrequiredMessage id, from GET /v1/messages.
curl -s -X DELETE https://api.email/v1/messages/66b2... -H "Authorization: Bearer ae_..."

200
{ "deleted": true }

GET /v1/messages/{id}/attachments/{n}

Download one attachment's bytes.

NameInRequiredDescription
idpathrequiredMessage id.
npathrequiredZero-based index into the message's "attachments" array.
curl -s https://api.email/v1/messages/66b2.../attachments/0 \
  -H "Authorization: Bearer ae_..." -o attachment.pdf

200 — raw bytes, original content-type and content-disposition headers

GET /v1/wait

Long-poll (up to 55s) for the next message matching the filter.

NameInRequiredDescription
namespacequeryoptionalRestrict to one namespace slug.
tagqueryoptionalExact match on the tag.
tag_prefixqueryoptionalPrefix match on tag; ignored if tag is set.
sincequeryoptionalDefault "now" — only mail that arrives after the call.
timeoutqueryoptionalSeconds, 1-55, default 30.
curl -s "https://api.email/v1/wait?tag=signup&timeout=30" \
  -H "Authorization: Bearer ae_..."

200 (message arrived)
{ "message": { "subject": "Your code", "extracted": { "otp_codes": ["482913"], ... } },
  "timed_out": false }

200 (timed out)
{ "message": null, "timed_out": true, "hint": "No email arrived in 30s. Retry ..." }

GET /v1/namespaces

List namespaces on this account.

No parameters.

curl -s https://api.email/v1/namespaces -H "Authorization: Bearer ae_..."

200
{ "namespaces": [ { "slug": "swift-otter-42", "domain": "swift-otter-42.api.email",
      "created_at": "2026-08-01T12:00:00.000Z" } ] }

POST /v1/namespaces

Create another namespace, up to the plan's limit.

NameInRequiredDescription
slugbodyoptionalOmit for a random slug.
curl -s -X POST https://api.email/v1/namespaces \
  -H 'content-type: application/json' -H "Authorization: Bearer ae_..." -d '{"slug":"acme"}'

201
{ "slug": "acme", "domain": "acme.api.email" }

GET /v1/keys

List API keys on this account (prefixes only, never the key itself).

No parameters.

curl -s https://api.email/v1/keys -H "Authorization: Bearer ae_..."

200
{ "keys": [ { "id": "66b3...", "prefix": "ae_AbCdEfGh", "created_at": "2026-08-01T12:00:00.000Z" } ] }

POST /v1/keys

Create an additional API key.

No parameters.

curl -s -X POST https://api.email/v1/keys -H "Authorization: Bearer ae_..."

201
{ "id": "66b3...", "api_key": "ae_...", "prefix": "ae_AbCdEfGh",
  "hint": "Store api_key now — it is shown only once." }

DELETE /v1/keys/{id}

Revoke an API key. Idempotent; refuses to revoke your only active key.

NameInRequiredDescription
idpathrequiredKey id, from GET /v1/keys.
curl -s -X DELETE https://api.email/v1/keys/66b3... -H "Authorization: Bearer ae_..."

200
{ "revoked": true }

GET /v1/usage

Current month email count and plan limits.

No parameters.

curl -s https://api.email/v1/usage -H "Authorization: Bearer ae_..."

200
{ "plan": "free", "month": "2026-08", "emails_received": 12,
  "limits": { "emails_per_month": 100, "namespaces": 1, "retention_days": 1 } }

POST /v1/billing/checkout

Start a Stripe Checkout session to upgrade to dev or pro.

NameInRequiredDescription
planbodyrequired"dev" or "pro".
emailbodyoptionalRequired only if the account has no email on file yet.
curl -s -X POST https://api.email/v1/billing/checkout \
  -H 'content-type: application/json' -H "Authorization: Bearer ae_..." -d '{"plan":"dev"}'

200
{ "url": "https://checkout.stripe.com/c/pay/..." }

POST /v1/billing/portal

Open the Stripe billing portal to change or cancel a plan.

No parameters.

curl -s -X POST https://api.email/v1/billing/portal -H "Authorization: Bearer ae_..."

200
{ "url": "https://billing.stripe.com/p/session/..." }

POST /v1/stripe/webhook — no auth required

Called by Stripe itself, not by API clients. Verified by the "stripe-signature" header.

NameInRequiredDescription
stripe-signatureheaderrequiredSet by Stripe; verified against STRIPE_WEBHOOK_SECRET.
200
{ "received": true }

MCP tools

Connect with claude mcp add --transport http api-email https://api.email/mcp (Streamable HTTP, POST /mcp). Authenticated tools return Authentication required if the connection has no valid bearer token: Call the signup tool first; it returns an api_key. Reconnect passing it as an "Authorization: Bearer ae_..." header, then retry.

signup — no auth required

Create a free api.email account: a namespace plus an api_key.

ArgumentRequiredDescription
emailoptionalOptional; must be a valid address if given.
> tool_call signup {}
< { "account_id": "66b1...", "namespace": "swift-otter-42", "api_key": "ae_...",
    "plan": "free", "example_address": "[email protected]",
    "hint": "Store api_key now — it is shown only once. ..." }

create_inbox

Generate a fresh unique address in your namespace. Any tag works without registration.

ArgumentRequiredDescription
tagoptionalOmit for a random tag.
> tool_call create_inbox { "tag": "signup-flow" }
< { "address": "[email protected]", "tag": "signup-flow",
    "namespace": "swift-otter-42", "hint": "Use this address ..., then call wait_for_message." }

get_usage

Current month email count and the plan limits for the authenticated account.

No arguments.

> tool_call get_usage {}
< { "plan": "free", "month": "2026-08", "emails_received": 12,
    "limits": { "emails_per_month": 100, "namespaces": 1, "retention_days": 1 } }

list_messages

List received emails, newest first. Filter by tag, tag_prefix, namespace, since.

ArgumentRequiredDescription
namespaceoptionalRestrict to one namespace slug.
tagoptionalExact tag match.
tag_prefixoptionalPrefix match on tag.
sinceoptionalEpoch ms or ISO 8601.
limitoptional1-100, default 10.
> tool_call list_messages { "tag": "signup-flow" }
< { "messages": [ { "id": "...", "subject": "Your code", "extracted": { "otp_codes": ["482913"] }, ... } ],
    "count": 1 }

get_message

Fetch one email by id (from list_messages/wait_for_message), including extracted OTP codes and links.

ArgumentRequiredDescription
idrequiredMessage id.
include_headersoptionalInclude raw email headers.
> tool_call get_message { "id": "66b2..." }
< { "message": { "id": "66b2...", "subject": "Your code",
      "extracted": { "otp_codes": ["482913"], "links": [], "verification_links": [] }, ... } }

wait_for_message

Long-poll for the next email matching the filter (up to 55s). since defaults to now.

ArgumentRequiredDescription
namespaceoptionalRestrict to one namespace slug.
tagoptionalExact tag match.
tag_prefixoptionalPrefix match on tag.
sinceoptionalEpoch ms or ISO 8601; defaults to now.
timeoutoptionalSeconds, 1-55, default 30.
> tool_call wait_for_message { "tag": "signup-flow", "timeout": 30 }
< { "message": { "subject": "Your code", "extracted": { "otp_codes": ["482913"] }, ... },
    "timed_out": false }

Errors

REST errors share one envelope: { "error": { "code", "message", "hint" } }, with an HTTP status matching code below.

Error codes
codestatusMeaningWhat to do
invalid_request400Request body or query parameters failed validation.Check the field named in "message" and retry.
unauthorized401Missing or invalid "Authorization: Bearer ae_..." header.Get a key from POST /v1/signup or POST /v1/keys.
not_found404The id or route does not exist, or belongs to another account.Re-fetch the id from a list endpoint; ids are account-scoped.
rate_limited429Too many requests to a rate-limited endpoint (e.g. signup: 10/hour/IP).Wait for the window to pass, then retry.
namespace_limit_reached403Your plan's namespace limit is already used.Upgrade the plan, or reuse an existing namespace.
slug_taken409The requested namespace slug is already in use.Pick a different slug, or omit it for a random one.
duplicate_key409API key hash collision (astronomically rare).Retry the request.
last_key409Attempted to revoke your only active API key.Create a new key first, then revoke the old one.
subscription_exists400The account already has an active subscription.Use POST /v1/billing/portal instead.
email_required400Checkout needs an email and the account has none on file.Pass {"email": "..."} in the request body.
no_billing_account400No Stripe customer exists yet for this account.Call POST /v1/billing/checkout first.
billing_unavailable503Billing is not configured on this server.Server operator: set the STRIPE_* env vars.
method_not_allowed405Wrong HTTP method on /mcp (only POST is supported).Use POST for MCP requests.
internal_error500Unexpected server error.Retry once; if it persists, report it.

MCP tool errors use a different shape: { isError: true, content: [{ type: "text", text: "{\"error\":\"\",\"hint\":\"\"}" }] } — check isError, not an HTTP status.

Limits & retention

Limits are enforced where the thing actually happens. Once monthly usage reaches the plan's email cap, the SMTP server rejects further incoming mail at RCPT TO with 552 Monthly email limit reached — nothing already stored is affected; delivery resumes next month or after an upgrade. Each message is also capped at 10MB, rejected with 552 too. Namespaces beyond the plan's limit are rejected at creation with 403 namespace_limit_reached. Messages are deleted automatically once they pass the plan's retention window. Check current usage with GET /v1/usage or the get_usage tool.

PlanEmails / monthNamespacesRetention
free10011 day
dev5,00037 days
pro25,0001030 days

Billing

Plans are managed through Stripe. POST /v1/billing/checkout with {"plan":"dev"} or {"plan":"pro"} returns a Checkout session URL; completing it upgrades the account once Stripe's webhook confirms payment. POST /v1/billing/portal returns a billing portal URL for changing or cancelling a plan (requires an existing Stripe customer, i.e. checkout must have run once). Both return 503 billing_unavailable if the server has no Stripe keys configured.

Plan changes also happen asynchronously via POST /v1/stripe/webhook, which Stripe calls directly — you never call it yourself.