Skip to content
Blog
Article
Developer guide · 11 min read

Temp email for developers — automating signup flows, OTPs, and email-based testing

How to wire disposable inboxes into your CI, your local dev loop, and your QA suite — including the rate-limit pitfalls that bite every team eventually.

Most teams discover they need a disposable email API the same way: someone writes a Cypress test that signs up a new user, then discovers the test infrastructure can't actually receive the confirmation email. They paste a fixed address into the test, watch their inbox fill with verification mails, and quietly resign themselves to flaky tests.

There's a better way. This post walks through the four production-grade patterns we've seen for using disposable mailboxes in code: signup flow automation, OTP plumbing for end-to-end tests, webhook-backed mail receivers, and on-demand addresses for one-shot scripts.

The four patterns

1. Signup flow automation (Cypress, Playwright, Selenium)

You're testing a signup flow that requires email verification. The test needs an address, then needs to read the verification mail.

Anti-pattern:

  • A static address (qa@example.com) shared across all tests — verification codes get crossed, tests run in any order, you can't parallelise.
  • A hard-coded password reset URL — works once, then the link expires and the test stops working.

Correct pattern:

// Cypress + Mail.tm
beforeEach(async () => {
  // Generate a unique inbox per test
  const { address, password } = await mailtm.createAccount();
  cy.wrap({ address, password }).as("inbox");
});

it("signs up a new user", () => {
  cy.get("@inbox").then(({ address }) => {
    cy.get("input[type=email]").type(address);
    cy.get("button[type=submit]").click();

    // Poll for the verification mail
    cy.wrap(null).then(() => {
      return waitForMessage(address, /verify your email/i);
    }).then((message) => {
      const link = extractLink(message.body, /verify\?token=/);
      cy.visit(link);
    });

    cy.contains("Welcome").should("be.visible");
  });
});

The key bits:

  • One inbox per test, generated in beforeEach.
  • A polling loop with timeout (don't use cy.wait(10000) — it's flaky).
  • A regex-based subject filter so unrelated mail (newsletter welcomes, etc.) doesn't match.

2. OTP plumbing

Many sign-in flows now require an OTP at every login. If the test runs against staging or a freshly-seeded environment, the OTP arrives via email and your test needs to read it.

For OTP specifically (always six digits, always within the first 200 characters of the body, almost always after a keyword like "code"), the simplest helper is:

async function getOtp(address: string, timeout = 30_000): Promise<string> {
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    const messages = await mailtm.getMessages(address);
    for (const m of messages) {
      const m6 = m.body.match(/\b\d{6}\b/);
      if (m6) return m6[0];
    }
    await sleep(500);
  }
  throw new Error("OTP did not arrive");
}

Things to handle in production:

  • The mailbox may already contain an old OTP from a previous run. Filter by message age (received_at > testStart) or burn the inbox between runs.
  • The mail body may contain other six-digit numbers (transaction IDs, dates). Prefer a regex anchored on a keyword: /code[:\\s]+(\\d{6})/i.
  • Some senders deliver the OTP as an HTML-only message. Strip HTML before regexing or pass the HTML through a sanitiser like sanitize-html.

3. Webhook-style receivers

For non-test workflows — say, you want every email sent to support+abc@yourdomain to ping a Slack channel — disposable providers won't cut it. You need a real forwarding service (Postmark, Resend, ImprovMX, ForwardEmail, your own Postfix).

But for a tightly-scoped use case — "run a webhook when an OTP arrives in this specific inbox" — Mail.tm's Mercure SSE endpoint is brilliant. You connect once, listen for message.created events, and your function fires within a second of the upstream IMAP delivery:

const eventSource = new EventSource(
  `https://mercure.mail.tm/.well-known/mercure?topic=/accounts/${accountId}`,
  { headers: { Authorization: `Bearer ${jwt}` } }
);

eventSource.onmessage = (e) => {
  const event = JSON.parse(e.data);
  if (event["@type"] === "message") {
    onMessage(event);
  }
};

4. On-demand addresses

Sometimes you just need an address inside a one-shot shell script — for example, you're benchmarking a new SaaS's signup flow, or scripting a competitor analysis.

#!/bin/bash
ADDR=$(curl -sX POST https://api.mail.tm/accounts \
  -H 'Content-Type: application/json' \
  -d "{\"address\":\"$(uuid)@mail.tm\",\"password\":\"x\"}" \
  | jq -r .address)

echo "Signed up as $ADDR"
# ... use $ADDR in your signup form ...

Rate limits — the real-world gotcha

Every public temp-mail service is shared infrastructure. Mail.tm has an 8 QPS budget across the entire IP range. If you spin up 200 Cypress tests in parallel, you will hit it. The provider's response is HTTP 429, sometimes a 5-minute cooldown.

Strategies:

  • Cap test concurrency at 4–8 if you're hitting one provider.
  • Use multiple providers — alternate Mail.tm and Mail.gw across test shards.
  • Rotate IPs (egress through different residential proxies). Most public providers rate-limit by IP; different IP = different budget.
  • Cache addresses across test runs and reuse where the test isolation allows.
  • For high-volume needs, pay for TempMail.lol's API tier or self-host a forwarding domain.

What about email content matching?

Don't scrape rendered HTML if you can avoid it — extract from the plain-text part. Most senders include both. The HTML is fragile across template changes; the plain text is usually stable for years.

If you must scrape HTML, use a headless DOM (jsdom, cheerio). Don't regex over raw HTML — you will eventually match a class name that contains the wrong digits.

Testing your own outbound mail

The mirror image of receiving mail in tests is verifying that your service sends correctly. For that, services like MailHog, Mailpit, or Mailtrap are better than disposable inboxes — they intercept SMTP locally and expose a UI for inspection. Use disposable inboxes only when your service genuinely sends through real SMTP and you need to verify external deliverability.

Plumbing this into PocketInbox

PocketInbox normalises the four major providers behind one REST API — same auth, same envelope shape, automatic failover when one provider rate-limits. The endpoints are stable and documented at our health endpoint shows the provider status.

We use it ourselves to test PocketInbox itself — the bootstrap test signs up a fake user, generates an inbox via our own API, sends mail to it, and verifies the entire round trip in under five seconds.

If you're building something that needs an inbox per test — give it a spin. If you hit a rate limit, the UI will switch you to a backup provider automatically.

Related reading: A guide to email OTP codes, Why disposable email gets blocked, and The best temp mail services in 2026.

Sponsored
Ad space (consent or AdSense ID required)

Continue reading

Read the FAQ · Back to PocketInbox