docs
v1 Pricing Dashboard
Getting started / quickstart-playwright

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.

terminal
$ npm install -D mailfixture

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.

.env / CI secret
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.

signup.spec.ts
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');
});
tip Need the magic link instead? 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.

terminal
$ 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.

POST — your endpoint receives
{
  "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.

verify the signature
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();
}
retries Non-2xx or slow (>5s)? We retry 8 times over ~21 hours — 30s, 2m, 10m, 30m, 2h, 6h, 12h. Five exhausted deliveries in a row auto-disables the endpoint (re-enable in the dashboard). Endpoints must be public https; delivery history is kept 7 days at GET /v1/webhooks/:id/deliveries.

Next steps

Custom domains
One MX record; never “disposable.”
Extraction
OTPs, links, and how we classify them.
Attachments
Metadata on every message; bytes one GET away.
Was this page useful? yes no Quickstart: Cypress →
ON THIS PAGE
edit this page ↗
report an issue ↗