fixture.email

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

  1. Mint an org-scoped API key in the keys portal.
  2. Connect the MCP where your agent lives:
claude-code · connect the mcp
claude mcp add --transport http fixture-email https://mcp.fixture.email/mcp --header "X-API-Key: $KEY"

Prefer raw HTTP? Same key, same calls:

bash · mint → send → assert
# 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 gets 403.
  • Revocation: revoke via the portal, DELETE /api/keys/{id}, or the revoke_api_key tool. 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.

RecipientStatusCallback
delivered@fixture.emailsentemail.sent
bounced@fixture.emailfailedemail.bounced
complained@fixture.emailsentemail.complained
suppressed@fixture.emailfailedemail.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.

EndpointPurpose
POST /api/sendBuild an .eml from JSON and queue it.
POST /api/send/uploadSend a raw .eml (multipart); optional From/To envelope overrides.
POST /api/attachments/uploadStage an attachment ≥ 1 MB (25 MB cap); returns an attachmentId.
POST /api/mailboxesMint 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}/enableRe-enable a disabled mailbox.
POST /api/mailboxes/{mailbox}/disableDisable a mailbox (rejects new mail immediately).
POST /api/mailboxes/{mailbox}/safe-cleanupSchedule a grace-period delete (optional duration).
POST /api/mailboxes/{mailbox}/cancel-cleanupCancel a scheduled cleanup.
GET /api/status/{id}One email's status + metadata (incl. SPF/DKIM/DMARC for inbound).
GET /api/messagesList/filter messages in a mailbox (polling).
GET /api/messages/{id}/contentParsed html / text / links / headers + auth results.
GET /api/messages/{id}/rawPresigned URL to download the raw .eml (expires in 15 min).
GET /api/usageOutbound quota + reputation snapshot (read-only).
POST · GET · DELETE /api/keysMint / list (metadata only) / revoke org API keys.
POST · GET · DELETE /api/callbacksRegister / list / remove webhook callbacks.
GET /api/openapi.json · /api/docsOpenAPI spec + Swagger UI (key-gated).
GET /healthLiveness (no auth).

Sending email

POST /api/send takes { from?, to, subject, text?, html?, cc?, bcc?, attachments?, headers? } and returns { emailId, mailbox, from }.

  • from is optional & smart: omit it to auto-create a mailbox; a bare local part becomes local+<newMailbox>@fixture.email; a full address carrying a valid +<mailbox> is used as-is (an unknown mailbox → 400).
  • to required; cc/bcc accept a string or an array.
  • Attachments: < 1 MB inline as base64 content; ≥ 1 MB pre-upload via /api/attachments/upload and reference by attachmentId (oversize base64 → 413).
  • headers carries X-* custom MIME headers only; they're stored in the .eml and returned (lowercased) from …/content.headers.
  • Raw replay: POST /api/send/upload sends an unmodified .eml (multipart), with optional from/to envelope overrides — handy for replaying a captured external message.
bash · POST /api/send
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).

URL-encode query params. Use 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 429 includes a checkoutUrl to 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/*403 until support restores it). You'll get an org.warning callback before the kill switch and org.suspended when 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-code · add the mcp
claude mcp add --transport http fixture-email https://mcp.fixture.email/mcp --header "X-API-Key: $KEY"
ToolWhat it does
whoamiOrg, plan, entitlement, trial end + outbound send usage.
create_mailboxMint a mailbox you own; returns an example inbound address.
list_messagesList a mailbox newest-first, with the same filters as /api/messages.
get_messageParse one message: subject, from/to, sanitized HTML, text, links, headers, auth results.
send_emailSend a test email (same payload as POST /api/send).
list_api_keysYour org's keys (metadata only — never the secret).
revoke_api_keyRevoke a key by id.
register_callbackRegister a signed webhook endpoint; returns the signing secret once.
list_callbacksList registered callbacks (never the secret).
delete_callbackDelete 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? } }.

text · verify a delivery
# 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).

CodeMeaning
400Invalid payload, unknown mailbox, or malformed URL encoding.
401Missing or invalid API key.
402Org not entitled (no live trial/subscription) — body carries a checkout URL.
403Recipient suppressed, or org suspended for abuse, or cross-org access.
404Not found (uniform with cross-org to avoid an enumeration oracle).
413Payload too large (base64 attachment > 1 MB, or upload > 25 MB).
429Over quota or rate-limited (a trial 429 carries a checkoutUrl).
500Server error.

Get these docs into your agent

Your agent is the user — hand it the docs the way it reads best: