Email testing API

Stop mocking email.
Test the real signup flow.

mailutils.dev gives every test its own inbox. Your app sends real email, we catch it, and your test grabs the one-time code or magic link in a single line, with no shared Gmail account, no flaky polling and no regexes in your test code.

Unlimited addresses OTP & link extraction Results in under 2 s
signup.spec.ts
import { MailUtils } from '@mailutils/sdk'

const mu = new MailUtils()   // reads MAILUTILS_API_KEY

test('user can sign up with an email code', async ({ page }) => {
  const inbox = await mu.newInbox()   // signup+x7k2@acme.mailutils.dev

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

  const code = await inbox.waitFor().otp()   // "482913"
  await page.fill('#code', code)
  await expect(page.getByText('Welcome')).toBeVisible()
})
Why it exists

Email is the flakiest step in your end-to-end tests

Signup, login, password reset and invites all go through an inbox. Most teams either skip those paths in CI or hack around them. mailutils.dev makes them as easy to test as an API call.

Shared test inboxes collide

Parallel CI runs read each other's emails from the same Gmail account. With mailutils, every run gets its own tagged address.

Sleep-and-poll is slow and flaky

sleep(10) then IMAP polling wastes time and still misses mail. Our wait endpoint returns the moment the email lands.

Parsing codes is brittle

Every auth provider formats emails differently. We extract the OTP and verification link for you, tested against 20 real-world templates.

Features

What you can use today

Everything below is live in the current release. Point an app at us and start testing.

Unlimited inboxes per project Live

Every project gets its own subdomain. Any address at *@acme.mailutils.dev works instantly, with no setup.

Plus-address tagging Live

user+run42@… lands in the user inbox tagged run42, so parallel test runs never see each other's mail.

SMTP sandbox Live

Point your app's SMTP settings at port 2525 with your API key. We capture everything and deliver nothing, so no real customer ever gets a test email.

Instant wait-for-email Live

One long-poll request that returns as soon as a matching email arrives, typically in under two seconds. No sleeps, no retry loops.

OTP & magic-link extraction Live

Codes like 482913, 842 017 or 7KQ-M2X and verify/sign-in/reset links come back as fields. Works with Auth0, Clerk, Cognito, Firebase, Supabase, Okta and more.

REST API Live

List, fetch, full-text search and delete messages. Get parsed HTML, text, headers, attachments metadata and the original .eml.

JavaScript / TypeScript SDK Live

await inbox.waitFor().otp() and you're done. Zero dependencies, works in Node 18+, Playwright, Cypress tasks and Vitest.

Web inbox viewer Live

See every caught email live: rendered HTML (scripts disabled), plain text, headers, links and raw source. Good for debugging a failing test.

API keys per project Live

Separate keys for CI, local dev and teammates. Keys are stored hashed and can be revoked at any time.

How it works

Three steps from send to assert

Your app sends an email

Either to a real address like signup+run42@acme.mailutils.dev (via our MX), or to any address through the SMTP sandbox on port 2525.

We catch and parse it

The message is stored, decoded (HTML, text, attachments) and scanned for one-time codes and verification links within milliseconds.

Your test reads the result

The SDK's waitFor() wakes up the moment the email lands and hands back the code, the link or the full message.

How we help you

Built for the people who own the test suite

QA & test automation engineers

Cover signup, email verification, passwordless login, password reset and team invites end to end, in Playwright or Cypress, without mocks or manual checks.

Developers building auth

Use the SMTP sandbox as your local and staging mail server. See exactly what your templates render and never email a real user by accident.

CI pipelines

Give every pipeline run, shard or worker its own tagged inbox. Tests stay isolated and run in parallel safely, with no cleanup between runs.

Teams shipping email features

Share one project across QA, dev and product, with per-person API keys and a web inbox everyone can open to see what went out.

Quickstart

Up and running in five minutes

Create a project and key, point your app at the sandbox, then wait for the email from your test.

// npm install @mailutils/sdk
import { MailUtils } from '@mailutils/sdk'

const mu = new MailUtils({ apiKey: process.env.MAILUTILS_API_KEY })

const inbox = await mu.newInbox('signup')       // unique tag per call
// ... trigger the email in your app using inbox.address ...

const otp  = await inbox.waitFor().otp()          // "482913"
const link = await inbox.waitFor().link()         // "https://app/verify?token=…"
const msg  = await inbox.waitFor({ timeout: 10_000 }) // full message
API reference

A small API that does one job well

Authenticate with Authorization: Bearer <api key>. Filters inbox, tag, to, q (full-text) and since work on list, wait and bulk delete.

EndpointWhat it does
GET /v1/messages/waitLong-polls until a matching email arrives (up to timeout seconds, max 60), then returns it with otp and primary_link. Returns 408 on timeout.
GET /v1/messagesLists messages newest first. Supports limit and before for pagination.
GET /v1/messages/{id}Full message: HTML, text, headers, attachments, links, extracted OTP.
GET /v1/messages/{id}/rawThe original RFC 822 source as .eml.
DELETE /v1/messages/{id}Deletes one message.
DELETE /v1/messagesBulk-deletes everything matching the filters, e.g. one test run's tag.
GET /v1/projectYour project's domain and SMTP sandbox settings.
Roadmap

What's coming next

We're building in the open with a small group of QA teams. Here's the plan for the next four months.

Proof of concept Live now

  • MX and SMTP sandbox ingest, plus-address tagging
  • REST API, long-poll wait, full-text search
  • OTP and link extraction, JS/TS SDK, web inbox

Private beta Dec 2026

  • Python SDK
  • Playwright and Cypress plugins
  • Signed webhooks on email arrival
  • Disposable, auto-expiring addresses per test run

v1 Jan 2027

  • Teams, invites and Stripe billing
  • Retention and quotas per plan
  • Custom domains via MX
  • Forward a caught email to an allowlisted real address
  • HTML compatibility, link and image checker
  • Spam score, SPF/DKIM/DMARC validator and header analyzer
  • GitHub Action: an inbox per workflow run

Public launch Feb 2027

  • Docs and example projects
  • Free public DNS tools (SPF, DMARC, MX lookups)
  • Uptime monitoring, abuse limits and support
Pricing

Free while in beta

Proposed plans for v1. Beta teams keep their pricing locked in.

Free

$0 / month
  • 1,000 emails / month
  • Unlimited addresses
  • API, SDK and web inbox
Sign up free

Team

$49 / month
  • 100,000 emails / month
  • Team members and invites
  • Release to real address, deliverability checks
FAQ

Questions

Does mailutils ever deliver email to real people?

No. Mail sent to the SMTP sandbox is captured and stored only. Nothing is relayed onward. Forwarding a caught email to an allowlisted address is planned for v1 and will be opt-in.

Which address should I use: MX or sandbox?

Use anything@yourproject.mailutils.dev when the app under test sends through a real provider (SendGrid, SES, Postmark…). Use the sandbox when you control the app's SMTP settings, for example in staging or CI.

How do I keep parallel test runs apart?

Add a tag: signup+<run-id>@…. The SDK's newInbox() does this for you and filters every wait by that tag.

Is it safe to open captured emails?

Yes. HTML is rendered in a sandboxed frame with scripts, forms and same-origin access disabled.

Can I self-host?

Yes. It's a single Go binary plus Postgres, and a Docker Compose file is included.