otpbox

OTPBox / Guides / Puppeteer

Puppeteer

Test email verification with Puppeteer

Puppeteer is often the tool behind a headless smoke test or a scraping-adjacent QA script, and those scripts hit the same wall as any other browser automation: a signup form that ends in "enter the code we emailed you." OTPBox gives a Puppeteer script a real disposable address and a real inbox to read from, so the OTP step gets exercised for real instead of skipped or stubbed.

The pattern

Same four steps as any framework: create a real inbox, use its address in the form Puppeteer is filling, block until the code or link arrives, then finish the flow and clean up. Below are two ways to do it — plain REST calls with Node's built-in fetch, or the first-party SDK.

Option 1: plain REST with fetch

No dependency beyond Puppeteer itself. Mint a free key once (200 requests/month, or an organization key for CI — see Get a key), then call the API directly in the script:

import puppeteer from 'puppeteer';

const key = process.env.OTPBOX_KEY;

const inboxRes = await fetch('https://otpbox.io/api/v1/inboxes', {
  method: 'POST',
  headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' },
  body: '{}',
});
const inbox = await inboxRes.json(); // { id, address, domain, expiresAt, token }

const browser = await puppeteer.launch();
const page = await browser.newPage();

try {
  await page.goto('https://your-app.example.com/signup');
  await page.type('[name="email"]', inbox.address);
  await page.click('button[type="submit"]');

  // Poll for the code (cap the retries in real code)
  let code = null;
  for (let i = 0; i < 15 && !code; i++) {
    await new Promise((r) => setTimeout(r, 1500));
    const msgs = await fetch(`https://otpbox.io/api/v1/inboxes/${inbox.id}/messages`, {
      headers: { authorization: `Bearer ${key}` },
    }).then((r) => r.json());
    code = msgs.messages[0]?.code ?? null;
  }

  await page.type('[name="otp"]', code);
  await page.click('button[type="submit"]');
  await page.waitForSelector('text/Welcome');
} finally {
  await browser.close();
  await fetch(`https://otpbox.io/api/v1/inboxes/${inbox.id}`, {
    method: 'DELETE',
    headers: { authorization: `Bearer ${key}` },
  });
}

Option 2: otpbox-sdk (recommended)

otpbox-sdk is the first-party TypeScript client, published on npm, and removes the manual polling loop with waitForOtp():

npm install otpbox-sdk puppeteer
import puppeteer from 'puppeteer';
import { OTPBox } from 'otpbox-sdk';

const client = new OTPBox({ apiKey: process.env.OTPBOX_KEY });
const inbox = await client.createInbox();

const browser = await puppeteer.launch();
const page = await browser.newPage();

try {
  await page.goto('https://your-app.example.com/signup');
  await page.type('[name="email"]', inbox.address);
  await page.click('button[type="submit"]');

  const code = await client.waitForOtp(inbox.id, { timeoutMs: 20_000 });
  if (!code) throw new Error('no OTP arrived before the timeout');

  await page.type('[name="otp"]', code);
  await page.click('button[type="submit"]');
  await page.waitForSelector('text/Welcome');
} finally {
  await browser.close();
  await client.deleteInbox(inbox.id);
}

waitForOtp() polls getMessages() until a message received after the call started has an extracted code, or the timeout elapses (returns null on timeout). There's also waitForEmail() for the whole message, and getUsage() if a script wants to check its remaining quota before running.

No fixture package yet. Playwright has an in-progress otpbox-playwright fixture; there's no equivalent otpbox-puppeteer package documented. Option 2 above — otpbox-sdk directly — is the supported path for Puppeteer.

Verifying via a link instead of a code

Some signup flows email a confirmation link rather than a 6-digit code. The same inbox works — use waitForEmail() and read the message's link, or fetch the full message for link.url:

const message = await client.waitForEmail(inbox.id, { timeoutMs: 20_000 });
const full = await client.getMessage(message.id);
await page.goto(full.link.url);
await page.waitForSelector('text/Verified');

CI notes

Next steps

Ready to try it against your own app? Create a free account

← Back to OTPBox