Read the docs
Give your agent a real inbox it can send to, receive in, and assert on — wired up in minutes. Programmable mailboxes on a real domain (SPF/DKIM/DMARC), a JSON send API, polling plus signed webhooks for assertions, and a built-in MCP. One org-scoped key authenticates both the MCP and direct HTTP.
Quickstart
- Mint an org-scoped API key in the keys portal.
- Connect the MCP where your agent lives:
claude mcp add --transport http fixture-email https://mcp.fixture.email/mcp --header "X-API-Key: $KEY"
Prefer raw HTTP? Same key, same calls:
# mint a mailbox your agent can send to and assert on
curl -sS -X POST https://send.fixture.email/api/mailboxes -H "X-API-Key: $KEY"
# send a test email to it (internal route — off-quota, no app wiring)
curl -sS -X POST https://send.fixture.email/api/send -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"to":"inbox+<id>@fixture.email","subject":"hello","text":"hi"}'
# then poll for it to arrive
curl -sS https://send.fixture.email/api/messages?mailbox=<id> -H "X-API-Key: $KEY"
Authentication
Every /api/* route — and the MCP — authenticates with a single
org-scoped API key. Send it as an X-API-Key header (or
Authorization: Bearer <key>). The key resolves to your
organization, which is the only authorization boundary: you only ever
see your org's mailboxes and messages.
- Where keys come from: a human mints them in the
keys portal (shown once), or you mint extra ones via
POST /api/keys(e.g. one per CI pipeline). Keys are never minted by an MCP tool. - Format:
feml_…. Treat it like a password; it is shown once and stored only as a hash. - Entitlement: a key whose org has no active subscription or live trial
is rejected with
402(the body carries a checkout URL). A suspended org gets403. - Revocation: revoke via the portal,
DELETE /api/keys/{id}, or therevoke_api_keytool. A revoked key may keep working for up to ~60s while the verification cache expires.
Core concepts
Mailboxes & sub-addressing
A mailbox is a random 16-character id you own. Any address of the form
<anything>+<mailbox>@fixture.email routes to it — so a single
mailbox catches many labelled addresses (signup+<mailbox>@…,
reset+<mailbox>@…). Point your app's "to" address at one of these and
assert on what lands.
The test loop
Create a mailbox → send to it (or trigger your real app's email to it) → poll for the message → read its parsed content to assert on subject, text, sanitized HTML, extracted links, headers, and SPF/DKIM/DMARC results.
Reads are polling (with optional webhooks)
There are no read webhooks: poll GET /api/status/{id} or
GET /api/messages every 2–3s (allow ~20–30 tries for external
sends). For push, register a signed lifecycle callback.
Quota
Only external sends count against quota; internal test-domain routing is
free and un-throttled. A send over the cap is rejected with 429
before it is queued — always read GET /api/usage for your exact,
current headroom.
Simulated outcomes
Send to a reserved sink on fixture.email to exercise outcome handling with
no real delivery (no deliverability or reputation impact): a terminal
status is synthesized and the matching callback fires.
| Recipient | Status | Callback |
|---|---|---|
delivered@fixture.email | sent | email.sent |
bounced@fixture.email | failed | email.bounced |
complained@fixture.email | sent | email.complained |
suppressed@fixture.email | failed | email.suppressed |
Simulated sinks are off-quota and can't be mixed with real recipients in one send.
HTTP API
Base URL https://send.fixture.email. Every /api/* route takes the
X-API-Key header. The OpenAPI spec is the
exhaustive, field-level source of truth; the highlights are below.
| Endpoint | Purpose |
|---|---|
POST /api/send | Build an .eml from JSON and queue it. |
POST /api/send/upload | Send a raw .eml (multipart); optional From/To envelope overrides. |
POST /api/attachments/upload | Stage an attachment ≥ 1 MB (25 MB cap); returns an attachmentId. |
POST /api/mailboxes | Mint a random 16-char mailbox id. |
GET /api/mailboxes/{mailbox} | A mailbox's current state (enabled, scheduledForCleanup, createdAt). |
DELETE /api/mailboxes/{mailbox} | Disable the mailbox (does NOT schedule cleanup). |
POST /api/mailboxes/{mailbox}/enable | Re-enable a disabled mailbox. |
POST /api/mailboxes/{mailbox}/disable | Disable a mailbox (rejects new mail immediately). |
POST /api/mailboxes/{mailbox}/safe-cleanup | Schedule a grace-period delete (optional duration). |
POST /api/mailboxes/{mailbox}/cancel-cleanup | Cancel a scheduled cleanup. |
GET /api/status/{id} | One email's status + metadata (incl. SPF/DKIM/DMARC for inbound). |
GET /api/messages | List/filter messages in a mailbox (polling). |
GET /api/messages/{id}/content | Parsed html / text / links / headers + auth results. |
GET /api/messages/{id}/raw | Presigned URL to download the raw .eml (expires in 15 min). |
GET /api/usage | Outbound quota + reputation snapshot (read-only). |
POST · GET · DELETE /api/keys | Mint / list (metadata only) / revoke org API keys. |
POST · GET · DELETE /api/callbacks | Register / list / remove webhook callbacks. |
GET /api/openapi.json · /api/docs | OpenAPI spec + Swagger UI (key-gated). |
GET /health | Liveness (no auth). |
Sending email
POST /api/send takes
{ from?, to, subject, text?, html?, cc?, bcc?, attachments?, headers? } and
returns { emailId, mailbox, from }.
fromis optional & smart: omit it to auto-create a mailbox; a bare local part becomeslocal+<newMailbox>@fixture.email; a full address carrying a valid+<mailbox>is used as-is (an unknown mailbox →400).torequired;cc/bccaccept a string or an array.- Attachments:
< 1 MBinline as base64content;≥ 1 MBpre-upload via/api/attachments/uploadand reference byattachmentId(oversize base64 →413). headerscarriesX-*custom MIME headers only; they're stored in the.emland returned (lowercased) from…/content.headers.- Raw replay:
POST /api/send/uploadsends an unmodified.eml(multipart), with optionalfrom/toenvelope overrides — handy for replaying a captured external message.
curl -sS -X POST https://send.fixture.email/api/send \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Reset your password",
"html": "<p><a href=\"https://app.example.com/r?t=abc\">Reset</a></p>",
"headers": { "X-E2E-Config": "reset-flow" }
}'
# -> { "emailId": "...", "mailbox": "x7k2…", "from": "...+x7k2…@fixture.email" }
Reading: status, messages & content
Poll GET /api/status/{id} for one email, or filter a mailbox with
GET /api/messages (filters: mailbox, to,
from, subaddress, subject,
receivedAfter, status). subject is a
literal substring match (no wildcards). time values are Unix
milliseconds; pair with millisecond-precision receivedAfter
(2026-03-08T06:33:10.634Z) so equal-second messages aren't dropped.
GET /api/messages/{id}/content returns parsed html (server
sanitized — <script>, on* handlers and
javascript:/data: URLs stripped), text,
links (all hrefs, entity-decoded), headers (keys lowercased),
and spfResult/dkimResult/dmarcResult for inbound
mail. …/raw returns a presigned URL to the original .eml (15 min).
URLSearchParams / encodeURIComponent (or curl
--data-urlencode); the server decodes. Malformed encoding → 400.Quota & reputation
Only external sends count. GET /api/usage returns your
authoritative, scaled caps and is discriminated by plan:
- Trial: 50 external sends/day, 250 total across the trial; a trial
429includes acheckoutUrlto upgrade. - Paid: 500/day and 15,000/month per purchased unit (buy N units at checkout to scale the cap).
- Suppression & auto-suspend: a real bounce or spam complaint adds the
recipient to your suppression list (a later send to it →
403+email.suppressed); sustained high bounce/complaint rates auto-suspend the org (all/api/*→403until support restores it). You'll get anorg.warningcallback before the kill switch andorg.suspendedwhen it trips.
MCP
The same org-scoped key authenticates a built-in Model Context Protocol
server, so an agent gets live mint / send / assert tools with no glue code. Connect to
https://mcp.fixture.email/mcp with the key as an X-API-Key header (or
Authorization: Bearer). Behavior matches the HTTP API exactly — same
send service, same ownership checks. On connect the server returns a short "how to test
email here" instruction set and a start-here prompt. Mint keys in the
portal, never via a tool.
claude mcp add --transport http fixture-email https://mcp.fixture.email/mcp --header "X-API-Key: $KEY"
| Tool | What it does |
|---|---|
whoami | Org, plan, entitlement, trial end + outbound send usage. |
create_mailbox | Mint a mailbox you own; returns an example inbound address. |
list_messages | List a mailbox newest-first, with the same filters as /api/messages. |
get_message | Parse one message: subject, from/to, sanitized HTML, text, links, headers, auth results. |
send_email | Send a test email (same payload as POST /api/send). |
list_api_keys | Your org's keys (metadata only — never the secret). |
revoke_api_key | Revoke a key by id. |
register_callback | Register a signed webhook endpoint; returns the signing secret once. |
list_callbacks | List registered callbacks (never the secret). |
delete_callback | Delete a callback by id. |
Webhooks (callbacks)
Register an HTTPS endpoint (POST /api/callbacks or the
register_callback tool) to receive signed, at-least-once
lifecycle events instead of polling. The signing secret is returned once.
URLs are SSRF-checked (https-only, no private hosts).
Events: email.sent, email.failed,
email.received, email.bounced, email.complained,
email.suppressed, plus the org-level org.warning and
org.suspended. Omit events on registration to subscribe to all.
Each delivery is a JSON POST:
{ id, event, occurredAt, data: { emailId, organizationId, mailbox?, status? } }.
# Headers on every delivery:
# X-Webhook-Id, X-Webhook-Timestamp (epoch ms), X-Webhook-Event, X-Webhook-Signature
X-Webhook-Signature = "sha256=" + hex( HMAC_SHA256( secret, timestamp + "." + rawBody ) )
# Recompute over the X-Webhook-Timestamp header + raw body, constant-time compare,
# reject stale timestamps, and dedupe on X-Webhook-Id (delivery is at-least-once).
Full payloads and status codes are in the OpenAPI spec.
Errors
Every error is { "error": "<message>" } — generic by design (no
internals leak).
| Code | Meaning |
|---|---|
400 | Invalid payload, unknown mailbox, or malformed URL encoding. |
401 | Missing or invalid API key. |
402 | Org not entitled (no live trial/subscription) — body carries a checkout URL. |
403 | Recipient suppressed, or org suspended for abuse, or cross-org access. |
404 | Not found (uniform with cross-org to avoid an enumeration oracle). |
413 | Payload too large (base64 attachment > 1 MB, or upload > 25 MB). |
429 | Over quota or rate-limited (a trial 429 carries a checkoutUrl). |
500 | Server error. |
Get these docs into your agent
Your agent is the user — hand it the docs the way it reads best:
- Copy as Markdown (button at the top) — paste this whole page straight into your agent or LLM.
- llms.txt — the machine-readable crib; point your agent at https://send.fixture.email/llms.txt.
- MCP — connect https://mcp.fixture.email/mcp for live mint / send / assert tools (X-API-Key auth).
- OpenAPI — the exhaustive, machine-readable spec at https://send.fixture.email/api/openapi.json (key-gated), with Swagger UI at https://send.fixture.email/api/docs.
- API catalog — /.well-known/api-catalog advertises these entry points for crawlers.