receive-only · built for CI

Email fixtures for your test suite.

Create a disposable inbox in one API call. Your app sends the OTP. We hand it to your test — parsed, typed, and on time.

100 messages/mo free · no per-seat pricing, ever
signup.spec.ts
import { test, expect } from '@playwright/test';
import { MailFixture } from 'mailfixture';

const mail = new MailFixture();

test('signup sends a one-time code', async ({ page }) => {
  // a real inbox: k3n9v0q2x7@mxsink.sh
  const inbox = await mail.createInbox();

  await page.goto('/signup');
  await page.fill('#email', inbox.address);
  await page.click('text=Send code');

  // long-polls until the email lands — no sleep()
  const otp = await inbox.waitForOtp();
  await page.fill('#otp', otp);

  await expect(page.locator('h1')).toHaveText('Welcome');
});
WORKS WITH Playwright Cypress pytest Selenium …or plain curl
01 · LONG-POLLING BUILT IN

Delete your retry loop.

waitForMessage() holds the connection open until the email lands — usually under a second after your app sends it. No sleep(5000), no polling every 500ms, no “flaky on CI, fine locally.”

the diff your teammates will thank you for
- await page.waitForTimeout(5000);- for (let i = 0; i < 10; i++) {-   msgs = await pollInbox(); if (msgs.length) break;-   await sleep(1000);- }+ const msg = await inbox.waitForMessage({ timeout: 30_000 });
GET /v1/messages/cb26ac0e-… · 200
{
  "subject": "Your Acme code",
  "to_addrs": ["qa-7f3a@tests.acme.dev"],
  "extracted": {
    "otp": { "best": "482913", "candidates": [] },
    "links": [
      { "class": "verify",
        "url": "https://acme.dev/verify?t=…" },
      { "class": "unsubscribe",
        "url": "https://acme.dev/unsub?u=…" }
    ]
  }
}
02 · FIRST-CLASS EXTRACTION

OTPs are fields, not regex homework.

Every message arrives pre-parsed: the one-time code, the magic link, the verification link — classified and sitting in extracted, next to SPF/DKIM/DMARC verdicts so your suite can assert the mail authenticates, not just arrives. Your assertion is one line, and it doesn't break when marketing redesigns the email. Codes arriving by text instead? Paid plans get dedicated US phone numbers, and SMS runs through the same extractor.

03 · CUSTOM DOMAINS

Never “disposable email detected.”

Shared test-mail domains end up on blocklists — then your signup form rejects your own tests. Point one MX record at us and every fixture lives on tests.yourdomain.com: unblockable, because it's yours.

DNS · two records, five minutes
MXtests → mx.mailfixture.com (10)
TXT_mfx.tests → mfx-verify=7f3a91c2
tests.acme.dev verified — fixtures armed
qa-7f3a@tests.acme.dev waiting for messages…
Your Acme code 0.9s after send
482913 copy otp
delivered → parsed → asserted· 1 passed, 0 flaky
04 · A DASHBOARD WORTH WATCHING

See the email your test saw.

Run a test, watch the inbox fill in real time. Rendered HTML, plain text, headers, raw source — and the extracted OTP one click from your clipboard when you're debugging by hand.

05 · MCP BUILT IN

Your AI agent gets its own inbox.

mailfixture is a remote MCP server. Connect Claude Code — or any MCP client — and your agent creates inboxes, triggers your signup flow, and reads the OTP back by itself. Same API key, every plan, nothing to install.

one command, then the agent takes it from there
$ claude mcp add --transport http mailfixture https://mailfixture.com/mcp \
    --header "Authorization: Bearer $MAILFIXTURE_API_KEY"

> test the signup flow end to end
  create_inbox()    k3n9v0q2x7@mxsink.sh
  wait_for_otp()    482913  ✓ verified
PRICING

No per-seat pricing. Ever.

Your whole team shares one plan — we count messages, not people. Go over on a paid plan? Soft overage at $3.50 per 1,000 messages — nothing shuts off mid-CI-run.

Free
$0
100 msgs/mo
shared domain
Solo
$15/mo
2,500 msgs/mo
custom domain
MOST TEAMS
Team
$49/mo
15,000 msgs/mo
custom domains
Scale
$149/mo
100,000 msgs/mo
5,000 active inboxes