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.
| Name | In | Required | Description |
|---|---|---|---|
email | body | optional | Optional; 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.
| Name | In | Required | Description |
|---|---|---|---|
namespace | query | optional | Restrict to one namespace slug. |
tag | query | optional | Exact match on the tag (local part before @). |
tag_prefix | query | optional | Prefix match on tag; ignored if tag is set. |
since | query | optional | Only messages received after this: epoch ms or ISO 8601. |
limit | query | optional | 1-100, default 10. |
offset | query | optional | Default 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.
| Name | In | Required | Description |
|---|---|---|---|
id | path | required | Message id, from GET /v1/messages. |
headers | query | optional | Pass "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.
| Name | In | Required | Description |
|---|---|---|---|
id | path | required | Message 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.
| Name | In | Required | Description |
|---|---|---|---|
id | path | required | Message id. |
n | path | required | Zero-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.
| Name | In | Required | Description |
|---|---|---|---|
namespace | query | optional | Restrict to one namespace slug. |
tag | query | optional | Exact match on the tag. |
tag_prefix | query | optional | Prefix match on tag; ignored if tag is set. |
since | query | optional | Default "now" — only mail that arrives after the call. |
timeout | query | optional | Seconds, 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.
| Name | In | Required | Description |
|---|---|---|---|
slug | body | optional | Omit 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.
| Name | In | Required | Description |
|---|---|---|---|
id | path | required | Key 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.
| Name | In | Required | Description |
|---|---|---|---|
plan | body | required | "dev" or "pro". |
email | body | optional | Required 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.
| Name | In | Required | Description |
|---|---|---|---|
stripe-signature | header | required | Set 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.
| Argument | Required | Description |
|---|---|---|
email | optional | Optional; 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.
| Argument | Required | Description |
|---|---|---|
tag | optional | Omit 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.
| Argument | Required | Description |
|---|---|---|
namespace | optional | Restrict to one namespace slug. |
tag | optional | Exact tag match. |
tag_prefix | optional | Prefix match on tag. |
since | optional | Epoch ms or ISO 8601. |
limit | optional | 1-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.
| Argument | Required | Description |
|---|---|---|
id | required | Message id. |
include_headers | optional | Include 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.
| Argument | Required | Description |
|---|---|---|
namespace | optional | Restrict to one namespace slug. |
tag | optional | Exact tag match. |
tag_prefix | optional | Prefix match on tag. |
since | optional | Epoch ms or ISO 8601; defaults to now. |
timeout | optional | Seconds, 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.
| code | status | Meaning | What to do |
|---|---|---|---|
invalid_request | 400 | Request body or query parameters failed validation. | Check the field named in "message" and retry. |
unauthorized | 401 | Missing or invalid "Authorization: Bearer ae_..." header. | Get a key from POST /v1/signup or POST /v1/keys. |
not_found | 404 | The id or route does not exist, or belongs to another account. | Re-fetch the id from a list endpoint; ids are account-scoped. |
rate_limited | 429 | Too many requests to a rate-limited endpoint (e.g. signup: 10/hour/IP). | Wait for the window to pass, then retry. |
namespace_limit_reached | 403 | Your plan's namespace limit is already used. | Upgrade the plan, or reuse an existing namespace. |
slug_taken | 409 | The requested namespace slug is already in use. | Pick a different slug, or omit it for a random one. |
duplicate_key | 409 | API key hash collision (astronomically rare). | Retry the request. |
last_key | 409 | Attempted to revoke your only active API key. | Create a new key first, then revoke the old one. |
subscription_exists | 400 | The account already has an active subscription. | Use POST /v1/billing/portal instead. |
email_required | 400 | Checkout needs an email and the account has none on file. | Pass {"email": "..."} in the request body. |
no_billing_account | 400 | No Stripe customer exists yet for this account. | Call POST /v1/billing/checkout first. |
billing_unavailable | 503 | Billing is not configured on this server. | Server operator: set the STRIPE_* env vars. |
method_not_allowed | 405 | Wrong HTTP method on /mcp (only POST is supported). | Use POST for MCP requests. |
internal_error | 500 | Unexpected server error. | Retry once; if it persists, report it. |
MCP tool errors use a different shape: { isError: true, content: [{ type: "text", text: "{\"error\":\" — 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.
| Plan | Emails / month | Namespaces | Retention |
|---|---|---|---|
| free | 100 | 1 | 1 day |
| dev | 5,000 | 3 | 7 days |
| pro | 25,000 | 10 | 30 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.