Skip to content
Blog
Article
Engineering · 9 min read

Automate signup flows in your tests with disposable email — a 2026 guide for QA engineers

End-to-end tests need real signup flows including email verification. Here's how to automate that with Mail.tm, TempMail.lol, and Cypress / Playwright in 2026.

Your end-to-end tests need to cover the signup flow — including the email-verification step. You can fake it (mock the SMTP layer or expose a backdoor), but that diverges your test environment from production. The robust alternative: drive a real disposable inbox via API.

The pattern

  1. Test starts: provision a fresh disposable inbox via API. Receive a one-time bearer token.
  2. Test uses that address to sign up to your app under test.
  3. Test polls the disposable-mail API for arrived messages.
  4. Test extracts the OTP / link from the email body.
  5. Test completes the verification flow.
  6. Cleanup (optional): delete the inbox.

Provider trade-offs for QA

  • Mail.tm — free, public API, 8 QPS shared rate limit. Fine for low-volume CI; rate-limit pain at high parallelism.
  • TempMail.lol — free + paid tiers; paid tier gives you a dedicated custom domain (no sharing rate limit, no chance of blocklist contamination from other users). Recommended for serious QA pipelines.
  • Mailinator paid — the most mature QA-targeted temp-mail; team subdomains, retention controls, official Cypress integration.

Cypress recipe (Mail.tm)

// cypress/support/commands.ts
Cypress.Commands.add('createTempMail', () => {
  return cy.request('POST', 'https://api.mail.tm/accounts', {
    address: `qa${Date.now()}@${Cypress.env('MAILTM_DOMAIN')}`,
    password: 'cypress-test-password-' + Date.now(),
  }).then((res) => {
    const address = res.body.address;
    return cy.request('POST', 'https://api.mail.tm/token', {
      address, password: 'cypress-test-password-' + res.body.id,
    }).then((tok) => ({ address, token: tok.body.token }));
  });
});

Cypress.Commands.add('waitForEmail', (token, timeoutMs = 30000) => {
  const start = Date.now();
  function poll() {
    return cy.request({
      url: 'https://api.mail.tm/messages',
      headers: { Authorization: `Bearer ${token}` },
    }).then((res) => {
      if (res.body['hydra:member'].length > 0) {
        return res.body['hydra:member'][0];
      }
      if (Date.now() - start > timeoutMs) {
        throw new Error('Email never arrived');
      }
      return cy.wait(2000).then(poll);
    });
  }
  return poll();
});

// In a test:
it('signs up and verifies email', () => {
  cy.createTempMail().then(({ address, token }) => {
    cy.visit('/signup');
    cy.get('[data-testid=email]').type(address);
    cy.get('[data-testid=submit]').click();
    cy.waitForEmail(token).then((msg) => {
      const otp = msg.text.match(/\b(\d{6})\b/)![1];
      cy.get('[data-testid=otp]').type(otp);
      cy.get('[data-testid=verify]').click();
      cy.url().should('include', '/dashboard');
    });
  });
});

Playwright recipe (TempMail.lol)

import { test, expect, request } from '@playwright/test';

test('email verification flow', async ({ page }) => {
  const ctx = await request.newContext();
  const inboxRes = await ctx.post('https://api.tempmail.lol/v2/inbox/create');
  const { address, token } = await inboxRes.json();

  await page.goto('/signup');
  await page.fill('[data-testid=email]', address);
  await page.click('[data-testid=submit]');

  let otp: string | undefined;
  for (let i = 0; i < 30; i++) {
    const inboxData = await ctx.get(`https://api.tempmail.lol/v2/inbox/${token}`);
    const data = await inboxData.json();
    if (data.messages?.length > 0) {
      const m = data.messages[0].body.match(/\b(\d{6})\b/);
      if (m) { otp = m[1]; break; }
    }
    await page.waitForTimeout(1000);
  }
  expect(otp).toBeDefined();
  await page.fill('[data-testid=otp]', otp!);
  await page.click('[data-testid=verify]');
  await expect(page).toHaveURL(/dashboard/);
});

Pitfalls and patterns

Rate limits

Mail.tm's 8 QPS limit is per-IP. CI runners on shared cloud IPs can hit each other. Fix: serialise tests that hit the same provider; or upgrade to TempMail.lol's paid tier.

Domain blocking

If your app has anti-disposable validation, your test will fail on signup. Two options: whitelist the QA domains in your test environment, or use TempMail.lol's custom-domain tier.

Polling vs webhooks

Polling is fine at QA scale. For higher throughput, TempMail.lol and Mailinator paid offer webhooks: receive a POST when mail arrives, parse it, signal the test. Reduces median test time by 5–10s.

Cleanup

Mail.tm: DELETE /accounts/{id}. Optional but polite. TempMail.lol: inboxes auto-expire after 1h. No cleanup needed.

OTP extraction

Don't over-engineer regex. Most OTPs are 6-digit codes; /\b(\d{6})\b/ matches reliably. For magic links, parse the first https:// URL in the body.

Don't do these

  • Don't hardcode real personal email addresses in CI. Privacy nightmare.
  • Don't use Gmail catchalls in CI — Google rate-limits aggressively and accounts get suspended.
  • Don't skip the verification step in CI by mocking it internally only — your prod-vs-test divergence will eventually ship a regression.

Related: Temp email for developers · Mail.tm provider deep-dive.

Sponsored
Ad space (consent or AdSense ID required)

Continue reading

Read the FAQ · Back to PocketInbox