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
- 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://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_mailbox → send_email →
wait_for_message (one call, no polling loop) → get_message.
Prefer raw HTTP? Same key, same calls:
# 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 theprovision_keystool, 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 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) → 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.
| 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://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.
| 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://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).
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 / 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 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_messagereturns the mail as soon as it arrives (immediately if it already has, so there's no race withsend_email), andwait_for_send_outcomeresolves 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_messagealso 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.
| 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. |
wait_for_message | Wait 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_message | Parse one message: subject, from/to, sanitized HTML, text, links, headers, auth results. Also renders it in clients that support MCP Apps. |
render_message_view | The render payload behind that viewer. Called by the viewer UI, not by you. |
send_email | Send a test email (same payload as POST /api/send). Returns once it is queued. |
wait_for_send_outcome | Wait for that send's terminal outcome (sent / failed / received) — one call, no webhook endpoint needed. |
list_api_keys | Your org's keys (metadata only — never the secret). |
revoke_api_key | Revoke a key by id (you are asked to confirm it by name and prefix). |
provision_keys | Author 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_builder | The mailbox list behind that builder. Called by the builder UI, not by you. |
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 (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? } }.
# 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://preview.fixture.email/llms.txt. It teaches the same flow this page does, in the form a model reads best.
- MCP — connect https://preview-mcp.fixture.email/mcp for live mint / send / wait / assert tools (X-API-Key auth), including the one-call waits and the in-client message viewer.
- OpenAPI — the exhaustive, machine-readable spec at https://preview-send.fixture.email/api/openapi.json (key-gated), with Swagger UI at https://preview-send.fixture.email/api/docs.
- API catalog — /.well-known/api-catalog advertises these entry points for crawlers.