Cypress Email Testing: Verify Signup and OTP Flows End-to-End
Cypress drives your UI beautifully, but the verification email in a signup flow lives outside the browser. Here’s a complete setup that signs a user up, fetches the verification email from a disposable inbox API, and submits the code — wrapped in reusable custom commands.
The approach
Cypress runs in the browser, but it can make HTTP requests with cy.request. We’ll talk to Devmailr’s API that way, give each test its own disposable inbox, and pull the code from the email that arrives.
Custom commands
Add three commands — create an inbox, wait for an email, delete the inbox:
const BASE = 'https://api.devmailr.app/api'
const auth = { Authorization: `Bearer ${Cypress.env('DEVMAILR_API_KEY')}` }
Cypress.Commands.add('createInbox', (prefix) =>
cy.request({ method: 'POST', url: `${BASE}/mailboxes`, headers: auth, body: { prefix } }).its('body'),
)
Cypress.Commands.add('waitForEmail', (mailboxId, query = '') =>
cy
.request({
url: `${BASE}/mailboxes/${mailboxId}/wait?timeout=60&${query}`,
headers: auth,
responseTimeout: 65000, // must exceed the 60s server wait
})
.its('body'),
)
Cypress.Commands.add('deleteInbox', (mailboxId) =>
cy.request({ method: 'DELETE', url: `${BASE}/mailboxes/${mailboxId}`, headers: auth }),
)Watch the timeout: Cypress’s default responseTimeout is 30s — shorter than a 60s wait. Set responseTimeoutabove your wait timeout (as above) so Cypress doesn’t give up before the email arrives.
The test
describe('signup', () => {
it('verifies a new account by email', () => {
cy.createInbox(`cy-${Date.now()}`).then((inbox) => {
// 1. Sign up with the disposable address
cy.visit('/signup')
cy.get('input[name=email]').type(inbox.address)
cy.get('input[name=password]').type('correct-horse-battery-staple')
cy.contains('button', 'Create account').click()
// 2. Wait for the verification email and read the code
cy.waitForEmail(inbox._id, 'has_code=true').then((email) => {
cy.get('input[name=code]').type(email.extractedCodes[0])
cy.contains('button', 'Verify').click()
cy.contains('Welcome').should('be.visible')
})
// 3. Clean up
cy.deleteInbox(inbox._id)
})
})
})Magic links
If your app emails a link instead of a code, grab primaryLink and visit it:
cy.waitForEmail(inbox._id, 'subject=' + encodeURIComponent('Confirm your email')).then((email) => {
cy.visit(email.primaryLink)
cy.contains('Email confirmed').should('be.visible')
})Tips
- Set the API key via Cypress env (
CYPRESS_DEVMAILR_API_KEYorcypress.env.json), never in the repo. - Unique inbox per test (
Date.now()) so parallel specs don’t share mail. - Reusing an inbox across steps? Pass
received_after=<ms>to ignore older mail. - Prefer Playwright? Same idea — see the Playwright guide.
Related reading
Try it in two minutes
Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.