Quickstart: Playwright
From zero to a green OTP test in about four minutes. You'll create an inbox, point your signup form at it, and assert on the code your app sends.
1 · Install
The SDK is a single dependency with zero transitive baggage.
$ npm install -D mailfixture
$ pip install mailfixture
# no install — curl ships with your OS. # you'll want jq for the examples below.
2 · Authenticate
Create a key in Dashboard → API keys, then export it. The SDK reads MAILFIXTURE_API_KEY automatically — never hardcode it in the repo your tests live in.
MAILFIXTURE_API_KEY=mfx_••••••••••••••••••••
3 · Write the test
One inbox per test keeps runs independent and parallel-safe. waitForOtp() long-polls the server — it resolves the moment the email lands, or throws after the timeout.
import { test, expect } from '@playwright/test'; import { MailFixture } from 'mailfixture'; const mail = new MailFixture(); // reads MAILFIXTURE_API_KEY test('signup sends a one-time code', async ({ page }) => { const inbox = await mail.createInbox({ ttlSeconds: 900 }); await page.goto('/signup'); await page.fill('#email', inbox.address); await page.click('text=Send code'); const otp = await inbox.waitForOtp({ timeout: 30_000 }); await page.fill('#otp', otp); await expect(page.locator('h1')).toHaveText('Welcome'); });
from mailfixture import MailFixture mail = MailFixture() # reads MAILFIXTURE_API_KEY def test_signup_sends_otp(page): inbox = mail.create_inbox(ttl_seconds=900) page.goto("/signup") page.fill("#email", inbox.address) page.click("text=Send code") otp = inbox.wait_for_otp(timeout=30) page.fill("#otp", otp) assert page.locator("h1").inner_text() == "Welcome"
# create an inbox $ curl -X POST https://api.mailfixture.com/v1/inboxes \ -H "Authorization: Bearer $MAILFIXTURE_API_KEY" \ -H "Content-Type: application/json" -d '{"ttl_seconds":900}' → {"id":"0d4cc81e-…","email_address":"k3n9v0q2x7@mxsink.sh",…} # …trigger your app's signup, then long-poll for the message: $ curl "https://api.mailfixture.com/v1/inboxes/0d4cc81e-…/messages?wait=45" \ -H "Authorization: Bearer $MAILFIXTURE_API_KEY" | jq -r .messages[0].id → cb26ac0e-… # the extracted OTP is its own endpoint: $ curl "https://api.mailfixture.com/v1/messages/cb26ac0e-…/otp" \ -H "Authorization: Bearer $MAILFIXTURE_API_KEY" | jq -r .best → 482913
waitForLink({ kind: 'verify' }) returns it already classified (verify, reset, unsubscribe). No regex was harmed.
4 · Run it
Locally or in CI — same behavior, because there's no sleep to tune. Keep the message viewer open while it runs; watching the email arrive never gets old.
$ npx playwright test signup.spec.ts Running 1 test using 1 worker ✓ signup sends a one-time code (1.4s) 1 passed (2.1s)
Webhooks — skip the polling (paid plans)
Long-polling is great; not calling us at all is better. Register an HTTPS endpoint in Dashboard → Webhooks (or POST /v1/webhooks) and we push a signed message.received event the instant mail lands — subject, sender, and the extracted OTP and links included. Message bodies never ride in webhooks; fetch the full message by id if you need it. Custom-domain owners also get domain.verified the moment DNS checks pass.
{
"id": "019f32f9-…", // delivery id — your idempotency key
"type": "message.received",
"created_at": "2026-07-05T15:50:42Z",
"data": {
"message": { "id": "…", "inbox_id": "…", "subject": "Your code is 482913", … },
"extracted": { "otp": { "best": "482913", … }, "links": […] }
}
}
Every request carries X-MailFixture-Signature: t=<unix>,v1=<hex> — HMAC-SHA256 over t.body with your endpoint's whsec_ secret (shown once at creation). Same scheme Stripe uses, so your existing verification muscle memory applies; both SDKs ship a helper.
import { verifyWebhookSignature } from 'mailfixture'; // in your handler — rawBody must be the unparsed request bytes if (!verifyWebhookSignature(rawBody, req.headers['x-mailfixture-signature'], process.env.MFX_WEBHOOK_SECRET)) { return res.status(400).end(); }
from mailfixture import verify_webhook_signature # raw_body must be the unparsed request bytes if not verify_webhook_signature(raw_body, headers["X-MailFixture-Signature"], os.environ["MFX_WEBHOOK_SECRET"]): return Response(status=400)
# header: t=1751700000,v1=<hex> — recompute and compare: $ printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$WHSEC" -hex
GET /v1/webhooks/:id/deliveries.