Cypress guide

Test verification emails and OTPs with Cypress

Cypress runs tests in the browser, so the MailBots API is called from a Node task instead. The key stays in Node, each test gets its own address on your catch-all subdomain, and the task returns the code once the verification email has arrived.

Before you start

  • An active Starter or Pro plan with a provisioned catch-all subdomain. See DNS onboarding.
  • An API key with the messages:read scope, created in Dashboard → Developer. See REST API.
  • Cypress 10 or later, running on Node.js 18 or later for the built-in fetch. The example was run on Cypress 16.1.

Both values are read by the Node task only, so neither reaches the browser under test:

MAILBOTS_API_KEY
mb_live_…
MAILBOTS_DOMAIN
otp.example.com
Note

Do not give either variable the CYPRESS_ prefix. Nothing here uses Cypress.env(), which Cypress 16 removed, so the same code runs from Cypress 10 to 16.

Add Node tasks

Two tasks run in Node. One creates a unique address, the other asks for the 50 newest messages and returns the code sent to that address, or null while it has not arrived. A Cypress task may not resolve to undefined, which is why "not yet" is null.

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on) {
      on('task', {
        mailbotsAddress(prefix) {
          if (!process.env.MAILBOTS_DOMAIN) throw new Error('MAILBOTS_DOMAIN is not set');
          const unique = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
          return `${prefix}-${unique}@${process.env.MAILBOTS_DOMAIN}`.toLowerCase();
        },
        async mailbotsOtp(address) {
          const response = await fetch('https://api.mailbots.top/v1/messages?limit=50', {
            headers: { Authorization: `Bearer ${process.env.MAILBOTS_API_KEY}` },
          });
          if (!response.ok) throw new Error(`MailBots API ${response.status}: ${await response.text()}`);
          const { data } = await response.json();
          const message = data.find((item) => item.envelope_to.toLowerCase() === address && item.otp);
          return message ? message.otp : null;
        },
      });
    },
  },
});

Add a polling helper

Cypress commands are queued, so the helper retries by chaining another task after a short wait instead of looping.

// cypress/support/mailbots.js
export function waitForOtp(address, attemptsLeft = 30) {
  return cy.task('mailbotsOtp', address, { log: false }).then((otp) => {
    if (otp) return otp;
    if (attemptsLeft <= 1) throw new Error(`No email with a code reached ${address}`);
    return cy.wait(2000, { log: false }).then(() => waitForOtp(address, attemptsLeft - 1));
  });
}

Write the test

Replace the URL and selectors with the ones in your app.

// cypress/e2e/signup.cy.js
import { waitForOtp } from '../support/mailbots';

describe('signup', () => {
  it('verifies a new account with an email code', () => {
    cy.task('mailbotsAddress', 'signup').then((address) => {
      cy.visit('/signup');
      cy.get('input[type=email]').type(address);
      cy.contains('button', 'Continue').click();

      waitForOtp(address).then((otp) => {
        cy.get('input[name=code]').type(otp);
      });
    });
    cy.contains('button', 'Verify').click();

    cy.contains('h1', 'Welcome').should('be.visible');
  });
});

Run it:

MAILBOTS_API_KEY=mb_live_REPLACE_ME MAILBOTS_DOMAIN=otp.example.com npx cypress run

Run it in CI

- name: Cypress
  run: npx cypress run
  env:
    MAILBOTS_API_KEY: ${{ secrets.MAILBOTS_API_KEY }}
    MAILBOTS_DOMAIN: otp.example.com

With 30 attempts two seconds apart, a test waits about a minute for mail before failing. Messages expire after 72 hours, so no cleanup step is needed.

Troubleshooting

SymptomWhat to check
Task fails with 401The key is revoked, lacks messages:read, or the plan is no longer active
Task fails with "MAILBOTS_DOMAIN is not set"The variable is missing from this run, or it was given the CYPRESS_ prefix
"Cypress.env() was removed"Code copied from an older version of this guide. Create the address with the mailbotsAddress task instead
Times out, message is in the dashboardMore than 50 messages arrived during the wait. Raise limit to 100
Message found but no codeNo convincing code was detected. Read the message in the dashboard