📱 OpenInbox is now on Android & iOS! Get the app today.

OpenInbox on Telegram

Get a temp inbox right inside Telegram.

Open Telegram bot

Custom domains

Use your own domain on paid plans.

Learn More
Email Testing • /cypress

/cypress Email Testing

Receive OTP codes and verification emails inside real Cypress E2E tests — using disposable inboxes from the OpenInbox REST API.

Email-dependent flows — signup confirmation, OTP login, password reset — are some of the hardest things to cover in Cypress, because the verification email lands in a real mailbox your test can't see. The fix is to give each test its own disposable inbox over the OpenInbox API, drive the UI with Cypress as usual, then poll the API for the email and pull the code or link out of its body. Every snippet below runs against the real OpenInbox API.

How the flow works

Cypress drives the browser; a small REST helper handles the mailbox. The whole loop — create, trigger, poll, extract, assert — usually completes in 10–20 seconds.

  • Create a disposable inbox: POST /api/inbox (no auth) returns { id, email, expiresAt }.
  • Type that address into your signup/login form and submit, so your app sends the email.
  • Poll GET /api/inbound/api/emails?inboxEmail=<email> with your X-API-KEY (premium) until an email arrives.
  • Extract the OTP (or confirmation link) from the email textBody with a regex.
  • Enter the code / visit the link in Cypress and assert the success state.

Setup

Install Cypress and cypress-recurse — the latter gives you a clean retry loop so you wait exactly as long as the email takes, instead of a brittle fixed cy.wait().

Polling the inbox requires a premium API key. Store it as a Cypress environment variable (never hard-code it): either in a git-ignored cypress.env.json, or as CYPRESS_OPENINBOX_API_KEY in your shell/CI.

terminalbash
npm i -D cypress cypress-recurse

# cypress.env.json  (add to .gitignore)
# { "OPENINBOX_API_KEY": "your-premium-api-key" }

# …or pass it at run time:
CYPRESS_OPENINBOX_API_KEY=your-premium-api-key npx cypress run

Add an OpenInbox custom command

Wrap the API in two Cypress commands so your specs stay readable. createInbox creates a fresh inbox; waitForOtp polls until an email lands and returns the extracted code. Because cy.request runs Node-side (not in the app under test), there are no CORS issues calling api.openinbox.io.

cypress/support/commands.tstypescript
import { recurse } from 'cypress-recurse';

const API = 'https://api.openinbox.io/api';

declare global {
  namespace Cypress {
    interface Chainable {
      createInbox(): Chainable<{ id: string; email: string }>;
      waitForOtp(inboxEmail: string): Chainable<string>;
    }
  }
}

// Create a disposable inbox — no auth required.
Cypress.Commands.add('createInbox', () => {
  return cy
    .request('POST', `${API}/inbox`)
    .its('body')
    .then((inbox) => ({ id: inbox.id, email: inbox.email }));
});

// Poll the inbox until an email arrives, then return the OTP from textBody.
Cypress.Commands.add('waitForOtp', (inboxEmail: string) => {
  return recurse(
    () =>
      cy.request({
        url: `${API}/inbound/api/emails?inboxEmail=${encodeURIComponent(inboxEmail)}`,
        headers: { 'X-API-KEY': Cypress.env('OPENINBOX_API_KEY') },
      }),
    (res) => res.body.emails.length > 0, // keep retrying until an email lands
    { timeout: 30000, delay: 2000 },
  ).then((res) => {
    const otp = res.body.emails[0].textBody.match(/\b\d{4,8}\b/)?.[0];
    expect(otp, 'OTP code in email body').to.exist;
    return otp as string;
  });
});

Write the OTP login test

With the commands in place, the test reads like the user story: create an inbox, request a code, wait for it, type it in, and assert you reached the dashboard.

cypress/e2e/otp-login.cy.tstypescript
describe('OTP login', () => {
  it('logs in with an emailed code', () => {
    cy.createInbox().then((inbox) => {
      cy.visit('/login');
      cy.get('[name="email"]').type(inbox.email);
      cy.contains('button', 'Send code').click();
      cy.contains('Check your email').should('be.visible');

      cy.waitForOtp(inbox.email).then((otp) => {
        cy.get('[data-testid="otp"]').type(otp);
        cy.contains('button', 'Verify').click();

        cy.url().should('include', '/dashboard');
        cy.get('[data-testid="user-menu"]').should('be.visible');
      });
    });
  });
});

Variant: password reset (extract a link)

The same pattern extracts a URL instead of a code — just change the regex you run against textBody. Here the poll is inlined with cypress-recurse so you can see the full shape.

cypress/e2e/password-reset.cy.tstypescript
import { recurse } from 'cypress-recurse';

const API = 'https://api.openinbox.io/api';

describe('Password reset', () => {
  it('resets via the emailed link', () => {
    cy.createInbox().then((inbox) => {
      cy.visit('/forgot-password');
      cy.get('[name="email"]').type(inbox.email);
      cy.contains('button', 'Send reset link').click();

      recurse(
        () =>
          cy.request({
            url: `${API}/inbound/api/emails?inboxEmail=${encodeURIComponent(inbox.email)}`,
            headers: { 'X-API-KEY': Cypress.env('OPENINBOX_API_KEY') },
          }),
        (res) => res.body.emails.length > 0,
        { timeout: 30000, delay: 2000 },
      ).then((res) => {
        const link = res.body.emails[0].textBody.match(/https?:\/\/\S*reset\S*/i)?.[0];
        cy.visit(link!);
        cy.get('[name="password"]').type('NewSecurePass123!');
        cy.contains('button', 'Reset password').click();
        cy.contains('Password updated').should('be.visible');
      });
    });
  });
});

Running it in CI (GitHub Actions)

Store your premium key as a repository secret and expose it to Cypress as CYPRESS_OPENINBOX_API_KEY. No SMTP servers or ports are involved — cy.request just makes HTTPS calls — so it works on any runner.

.github/workflows/e2e.ymlyaml
name: E2E
on: [push]
jobs:
  cypress:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx cypress run
        env:
          CYPRESS_OPENINBOX_API_KEY: ${{ secrets.OPENINBOX_API_KEY }}

Common pitfalls

  • Don't poll with a fixed cy.wait(5000) — emails can take 2–10s and a fixed wait is either flaky or slow. Use cypress-recurse (or a recursive cy.request) so the test waits exactly as long as needed, up to your timeout.
  • Creating an inbox needs no auth, but the inbound emails endpoint is premium — a 401/403 from /inbound/api/emails means the X-API-KEY is missing or the account is not on a paid plan.
  • The /\b\d{4,8}\b/ regex matches any 4–8 digit run, so it can catch a year or an order number. If your email contains other numbers, anchor it (e.g. /code is\s*(\d{6})/i).
  • Free inboxes expire after one hour. That is plenty for a test run, but create a fresh inbox per test rather than reusing one across a long suite.
  • cy.request runs from Cypress (Node), not the browser, so calls to api.openinbox.io are not subject to your app's CORS policy — you do not need to proxy them.

Frequently Asked Questions

Can Cypress read emails directly?

No — Cypress runs in the browser and can't open a real mailbox. Instead you create a disposable inbox via the OpenInbox API and poll it with cy.request, which keeps the whole flow inside your Cypress test.

Do I need an API key for this?

Creating an inbox (POST /api/inbox) is unauthenticated. Polling for received emails (GET /api/inbound/api/emails) requires a premium X-API-KEY, so store one as CYPRESS_OPENINBOX_API_KEY.

How long does it take for the email to arrive?

Usually 2–5 seconds. Poll with cypress-recurse using a ~2s delay and a 30s timeout, which is comfortable for CI while keeping tests fast.

How do I extract the OTP code in Cypress?

Run a regex against the email textBody field, for example res.body.emails[0].textBody.match(/\b\d{4,8}\b/)?.[0]. The API returns the raw textBody, so OTP extraction is always a one-line regex.

Can I run these tests in parallel?

Yes. Create one inbox per test (cy.createInbox), so each spec has an isolated mailbox and parallel workers never see each other's emails.

Will this work in CI / GitHub Actions?

Yes — cy.request only makes HTTPS calls to api.openinbox.io (no SMTP, no open ports), so it runs on any CI runner. Pass the key via the CYPRESS_OPENINBOX_API_KEY secret.

Related Pages

Explore More

Your private inbox is one click away

Free, instant, no registration needed. Works with any service. Expires automatically after 1 hour.

View API Docs