Webhooks
HTTPS push for the moments something arrives: signed deliveries with honest retries, and a hard rule that message bodies never ride in them.
When to use them (and when not to)
Tests should long-poll — a wait= request is simpler, faster, and needs no public endpoint. Webhooks are for the glue around tests: notifying a Slack channel, kicking a downstream job when a domain verifies, feeding messages into a pipeline. Webhooks ship on paid plans — 3 endpoints on solo, 10 on team, 25 on scale.
Events
Three event types today: message.received (an email landed — summary fields plus extraction results), sms.received (same, for inbound SMS), and domain.verified (a custom domain finished DNS verification).
One rule with no exceptions: bodies never ride in webhooks. Payloads carry subjects, senders, and extracted OTPs and links — not text_body, not html_body, not an SMS text. Your email is customer data and your webhook receiver's logs shouldn't become a copy of it; when a consumer needs content, it fetches the message by id over the authenticated API.
The envelope
{
"id": "6b9f42e0-…", // delivery id — your idempotency key
"type": "message.received",
"created_at": "2026-07-07T14:18:02Z",
"data": { …summary + extracted… }
}
id is the delivery's UUID and stays the same across retries of that delivery — deduplicate on it and retries become harmless. It also arrives as the X-MailFixture-Delivery header.
Signatures
Each endpoint gets a whsec_… secret, shown once at creation. Every delivery is signed:
X-MailFixture-Signature: t=1751897882,v1=5257a869e7…
v1 is HMAC-SHA256(secret, "<t>.<body>") in hex — deliberately the same scheme Stripe uses, so verification code you already run for Stripe ports by changing the header name. Both SDKs ship a verify_webhook_signature helper. Verify on every request, and reject stale timestamps (a few minutes is plenty) to shut out replays.
Delivery and retries
Endpoints must be HTTPS on a publicly routable address; deliveries follow no redirects and time out after 5 seconds. Answer any 2xx to acknowledge — do the real work async if it's slow.
Anything else is retried with backoff: up to 8 attempts at roughly 30s, 2m, 10m, 30m, 2h, 6h, and 12h after the first — about 21 hours of trying. If 5 deliveries in a row exhaust all attempts, the endpoint is auto-disabled so a dead receiver doesn't burn retries forever; fix it, then re-enable from the dashboard or POST /v1/webhooks/{id}/enable.
Each endpoint keeps 7 days of delivery history — every attempt with its response code — in the dashboard and at GET /v1/webhooks/{id}/deliveries. POST /v1/webhooks/{id}/test fires a test event to check your wiring before anything real depends on it.
test event and the delivery history side by side: send, look at the recorded response code, fix, repeat. It's the fastest debugging loop we know for webhook plumbing.