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, signed webhooks, and a built-in MCP whose agent waits for an email in one call instead of polling. 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://preview-mcp.fixture.email/mcp --header "X-API-Key: $KEY"

One URL and one header — there's no session to open and nothing to keep alive. Your agent then runs create_mailboxsend_emailwait_for_message (one call, no polling loop) → get_message.

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://preview-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://preview-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 (HTTP only — over the MCP this is one wait_for_message call)
curl -sS https://preview-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). An agent can author a scoped key with the provision_keys tool, but it gets back an inert claim link — a secret is revealed once in the browser and never by a 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) → wait for the message → read its parsed content to assert on subject, text, sanitized HTML, extracted links, headers, and SPF/DKIM/DMARC results.

Waiting: one call over the MCP, polling over HTTP

Over the MCP the wait is a single call: wait_for_message returns the message as soon as it lands (and immediately if it already has), and wait_for_send_outcome resolves a send to sent / failed / received. Don't write a retry loop around list_messages.

Over plain HTTP there is no waiting call and 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://preview-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://preview-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

This is the HTTP path; over the MCP the same wait is a single wait_for_message / wait_for_send_outcome call. 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 / wait / assert tools with no glue code. Connect to https://preview-mcp.fixture.email/mcp with the key as an X-API-Key header (or Authorization: Bearer) — one URL, one header, no session and no handshake; each request is authenticated and entitlement-checked on its own. On connect the server hands your agent a short "how to test email here" instruction set and a start-here prompt, both describing the same flow as llms.txt. Behavior matches the HTTP API exactly — same send service, same ownership checks.

claude-code · add the mcp
claude mcp add --transport http fixture-email https://preview-mcp.fixture.email/mcp --header "X-API-Key: $KEY"

Two things the MCP gives you that raw HTTP can't:

  • You never write the retry loop. wait_for_message returns the mail as soon as it arrives (immediately if it already has, so there's no race with send_email), and wait_for_send_outcome resolves a send that came back merely queued. Expiry is a normal result carrying a cursor to call again with — not an error.
  • You can look at the email. In a client that supports MCP Apps (Claude, Claude Desktop, VS Code Copilot and the like) get_message also renders the message — body, links, headers, SPF/DKIM/DMARC — with remote content blocked by default and every blocked URL listed, so "what did this email try to load?" is answered without firing a single request. A client without Apps support gets exactly the same JSON result it always did.
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.
wait_for_messageWait for a message and return it as soon as it lands — one call, no polling loop. Filter by from / subject / subaddress; expiry returns a cursor, not an error.
get_messageParse one message: subject, from/to, sanitized HTML, text, links, headers, auth results. Also renders it in clients that support MCP Apps.
render_message_viewThe render payload behind that viewer. Called by the viewer UI, not by you.
send_emailSend a test email (same payload as POST /api/send). Returns once it is queued.
wait_for_send_outcomeWait for that send's terminal outcome (sent / failed / received) — one call, no webhook endpoint needed.
list_api_keysYour org's keys (metadata only — never the secret).
revoke_api_keyRevoke a key by id (you are asked to confirm it by name and prefix).
provision_keysAuthor scoped keys from a grant spec; returns inert portal claim links, never a secret. Also opens a grant builder in clients that support MCP Apps.
render_grant_builderThe mailbox list behind that builder. Called by the builder UI, not by you.
register_callbackRegister a signed webhook endpoint; returns the signing secret once.
list_callbacksList registered callbacks (never the secret).
delete_callbackDelete a callback by id (you are asked to confirm it by URL).

Keys are minted by a human in the portal; the provision_keys tool can author a scoped key, but only ever returns a claim link a human opens to reveal the secret once.

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: