One inbox per worker
POST /v1/inboxes hands back a fresh address. Twelve workers running in parallel never read each other’s mail, so retries stop being a coin flip.
Mailwurf is a hosted catch all mailbox for end to end tests. Your app keeps sending through Resend or SES. Your test creates an address, waits for the message, and opens the link.
No mail server to run. No DNS to hold. One API key per project.

const inbox = await mailwurf.createInbox();
// k3f9x2@mailwurf.io
await page.fill("[name=email]", inbox.address);
await page.click("text=Sign up");
const mail = await inbox.waitFor({
subject: "Confirm",
});
await page.goto(mail.links[0]);The whole integration. No polling loop, no shared mailbox, no sleep.
The moment a test rewires where mail goes, it stops covering the part that actually breaks: the provider, the template, the link. Mailwurf changes the recipient instead.
POST /v1/inboxes hands back a fresh address. Twelve workers running in parallel never read each other’s mail, so retries stop being a coin flip.
GET /messages/wait blocks until the message arrives, up to 120 seconds. Matching is oldest first, by subject substring, and after takes a message id.
Each message comes back with text, html, links and the detected OTP. The raw MIME stays in the store, because parsers are wrong sometimes.
Mail reaches Mailwurf the way it reaches a customer: through your provider, over MX, with your templates. A broken sender fails the test, not the release.
We hold the MX records, the certificate and the store. You hold an API key. There is no mail server in your stack and no port 25 in your firewall rules.
The /v1 contract works from any test runner. Use the small TypeScript client or call it directly with a project API key.
Every project gets its own key. One environment variable in the test runner, and nothing about the app under test changes.
MAILWURF_API_KEY=mw_live_9c2f41d8a7One call per test, before the flow starts. You get an id and an address on mailwurf.io. Drop the address into the form the way a real signup would.
const mailwurf = createClient(); // reads the key
const inbox = await mailwurf.createInbox();
await page.fill("[name=email]", inbox.address);The request stays open until a matching message arrives. Subject is a substring match, oldest message first, and the response carries the parsed links and OTP.
const mail = await inbox.waitFor({
subject: "Confirm your email",
timeout: 15_000,
});
expect(mail.otp).toMatch(/^\d{6}$/);
await page.goto(mail.links[0]);Every call under /v1 takes your project key as a bearer token. The TypeScript client is a fetch wrapper around these calls, so anything that speaks HTTP works the same way.
| Method | Path | Notes |
|---|---|---|
| GET | /llm | Public Markdown guide for agents |
| GET | /llms.txt | Same body as /llm |
| POST | /v1/inboxes | Create a temporary inbox |
| GET | /v1/inboxes?address= | Look up an inbox by address |
| GET | /v1/inboxes/{id}/messages | Filter by subject and after |
| GET | /v1/inboxes/{id}/messages/wait | Server-side wait, timeout in ms |
| GET | /v1/messages/{id} | One parsed message |
| DELETE | /v1/inboxes/{id} | Delete a temporary inbox |
| GET | /v1/health | Public API readiness |
{
"id": "01J8ZQ2K7M",
"subject": "Confirm your email",
"from": "hello@yourapp.com",
"text": "Your code is 418902",
"links": ["https://yourapp.com/confirm?t=ey..."],
"otp": "418902",
"receivedAt": "2026-08-20T19:04:11Z"
}Parsed fields are a convenience. The original message is stored next to them, so a template change that confuses the parser never silently changes what your assertions see.
Open the API docscurl -s -X POST https://api.mailwurf.io/v1/inboxes \
-H "Authorization: Bearer $MAILWURF_API_KEY"
# {"id":"01J8ZQ…","address":"k3f9x2@mailwurf.io"}Anything that speaks HTTP works. The TypeScript client is a fetch wrapper around exactly these calls, with types for the message shape.
MAILWURF_API_KEY=mw_live_…Mailwurf is in private access while we finish the billing side. Tell us how your suite runs and we will tell you which plan fits and what it will cost.
For the first spec, on a shared mailwurf.io domain.
Pricing to be announced
For a suite that runs on every pull request.
Pricing to be announced
For teams that need their own MX name in the address.
Pricing to be announced
Those are SMTP sinks you point your app at, so the test never exercises Resend, SES, your templates or your DNS. Mailwurf receives on real MX records, which means only the recipient address changes and the rest of the path stays production.
No. The app keeps its provider, its credentials and its templates. The test sets the recipient to an inbox address it created a moment earlier.
No. We hold the MX records, the TLS certificate and the store. Your side is one environment variable in the test runner and a call at the start of the spec.
It is refused with SMTP 550. Tests create temporary inboxes through the API first, while owners and admins can register up to five permanent addresses per organization.
Every call under /v1 requires a project key, every lookup is scoped to that key, and messages are deleted when their retention window ends. Treat Mailwurf as a test system regardless: it is built for mail your test suite generates, not for production mail or real customer data.
Yes. The MX endpoint offers STARTTLS with a certificate covering the MX name, which is what providers check before they hand a message over. Nothing about your own SPF or DKIM setup needs to change.
The timeout query parameter is in milliseconds, defaults to 15000 and is capped at 120000. Matching is oldest message first, subject is a substring, and after takes a message id so a retry does not resolve to the message you already read.
Yes. It is plain HTTP under /v1 with a project bearer token. The TypeScript client is optional.
Anything that can make an HTTP request works: Cypress, Vitest, Go tests, a shell script with curl. The TypeScript client is a thin fetch wrapper with no Playwright dependency.
Yes, on the dedicated plan. You delegate an MX record for a subdomain to us and inboxes are created under that name instead of mailwurf.io.
Until the inbox TTL for your plan elapses, or until you call DELETE on the inbox, whichever comes first. Test mail is deleted, not archived.
Pricing is not public yet. Mailwurf is in private access while billing is finished, so write to us with the shape of your suite and we will tell you which plan fits.
Send us the shape of your suite and we set up a project key. One call in the spec, and the mail step stops being the flaky one.
