Automate signup flows in your tests with disposable email — a 2026 guide for QA engineers
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
- Test starts: provision a fresh disposable inbox via API. Receive a one-time bearer token.
- Test uses that address to sign up to your app under test.
- Test polls the disposable-mail API for arrived messages.
- Test extracts the OTP / link from the email body.
- Test completes the verification flow.
- 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.
Continue reading
- The best temp mail services in 2026 — a developer-friendly comparison
- Temp email for developers — automating signup flows, OTPs, and email-based testing
- Temp mail vs VPN vs email aliases — what each one actually does for your privacy
- How to receive email without a phone number — every legal way that actually works