TL;DR: To test OTP and 2FA emails in an automated suite, send the email to a private test domain, fetch the message through an API, extract the one-time passcode with a regular expression, and type it into your app. This lets Cypress, Playwright, or any framework complete verification flows end to end without a human reading an inbox.
One-time passcodes are one of the hardest things to automate because the code is generated at runtime and delivered by email. The reliable pattern is to give your tests a mailbox they can read programmatically. Use a private testing domain rather than a public inbox so codes stay private and addresses never collide between parallel test runs. Mailinator’s Verified Pro tier includes a private domain and API access for free.
Why is testing OTP and 2FA emails hard?
OTP emails are difficult to automate because the passcode is dynamic, short-lived, and delivered asynchronously. Your test cannot know the code in advance, a real inbox is slow and manual to check, and public addresses risk collisions when tests run in parallel. The solution is a mailbox your test can query by API and an extraction step that pulls the code from the message body.
How do you test an OTP email in an automated test?
Testing an OTP email takes four steps: trigger the email to a unique test address, poll the inbox API until the message arrives, extract the passcode from the body with a regular expression, then submit it in your application and assert success. The same pattern works across Cypress, Playwright, Selenium, and Puppeteer — only the syntax changes.
- Generate a unique address on your private domain, e.g.
signup-{runId}@yourteam.testinator.com. - Complete the signup or login step so your app sends the OTP.
- Fetch the newest message for that address from the inbox API.
- Extract the code with a regex and submit it, then assert the verified state.
How do you extract an OTP with Playwright?
In Playwright, use the built-in request API to poll the inbox, then a regular expression to pull the six-digit code out of the message. Because Playwright awaits network calls natively, you can wrap the fetch in a short retry loop to handle delivery delay without flakiness.
import { test, expect } from '@playwright/test';
async function getOtp(request, inbox) {
for (let i = 0; i < 10; i++) {
const res = await request.get(
`https://api.example-inbox.com/v2/inboxes/${inbox}`,
{ headers: { Authorization: process.env.MAIL_API_TOKEN } }
);
const { messages } = await res.json();
if (messages?.length) {
const body = messages[0].parts[0].body;
const match = body.match(/\b(\d{6})\b/);
if (match) return match[1];
}
await new Promise(r => setTimeout(r, 2000));
}
throw new Error('OTP email did not arrive in time');
}
test('user verifies with OTP', async ({ page, request }) => {
const inbox = `signup-${Date.now()}`;
await page.goto('https://yourapp.com/signup');
await page.fill('#email', `${inbox}@yourteam.testinator.com`);
await page.click('#submit');
const otp = await getOtp(request, inbox);
await page.fill('#otp', otp);
await page.click('#verify');
await expect(page.getByText('Account verified')).toBeVisible();
});
How do you extract an OTP with Cypress?
In Cypress, use cy.request() to call the inbox API and a recursive command to retry until the email lands. Extract the passcode with the same regular expression, then drive it back into the UI. Keep the token in Cypress.env() rather than hard-coding it.
Cypress.Commands.add('getOtp', (inbox, attempt = 0) => {
return cy.request({
url: `https://api.example-inbox.com/v2/inboxes/${inbox}`,
headers: { Authorization: Cypress.env('MAIL_API_TOKEN') },
}).then((res) => {
const msg = res.body.messages?.[0];
const code = msg && msg.parts[0].body.match(/\b(\d{6})\b/);
if (code) return code[1];
if (attempt > 9) throw new Error('OTP not received');
cy.wait(2000);
return cy.getOtp(inbox, attempt + 1);
});
});
it('verifies a new account with OTP', () => {
const inbox = `signup-${Date.now()}`;
cy.visit('https://yourapp.com/signup');
cy.get('#email').type(`${inbox}@yourteam.testinator.com`);
cy.get('#submit').click();
cy.getOtp(inbox).then((otp) => {
cy.get('#otp').type(otp);
cy.get('#verify').click();
cy.contains('Account verified').should('be.visible');
});
});
What are best practices for OTP email testing?
Reliable OTP tests share a few habits: isolate every run on its own address, retry the fetch with a timeout instead of a fixed wait, and scope the regex tightly so it matches the code and nothing else. Keeping tests on a private domain removes the biggest source of flakiness — shared inboxes and blocked disposable addresses.
- Use a unique address per test run to prevent collisions in parallel CI.
- Poll with a retry-and-timeout loop, not a hard-coded
wait. - Anchor your regex (for example
\b\d{6}\b) so it grabs only the passcode. - Store API tokens as environment variables, never in the test file.
- Prefer a webhook over polling when your platform supports it for faster, steadier runs.
Frequently asked questions
Can you automate 2FA email testing without a real inbox?
Yes. A testing mailbox with an API replaces a real inbox: your test sends the 2FA email to a private test address, reads the message over the API, and extracts the code. No human ever opens an inbox, so the flow runs unattended in CI.
How do you extract a one-time passcode from an email body?
Fetch the message through the inbox API and run a regular expression against the body. For a six-digit numeric code, /\b\d{6}\b/ is usually enough; adjust the pattern to match your code’s length and format, and read the HTML or plain-text part depending on how your email is built.
Why use a private domain for OTP testing instead of a public inbox?
A private domain keeps passcodes visible only to your team, prevents two parallel tests from landing on the same public address, and avoids signup forms rejecting known disposable domains. It is the difference between an OTP suite that passes reliably and one that flakes under load.
Want a mailbox your tests can read by API? Start a free Verified Pro account, point your signup flow at your private domain, and automate OTP and 2FA end to end.