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.
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.
fetchNo 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}` },
});
}
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.
otpbox-playwright fixture; there's no equivalent otpbox-puppeteer package documented. Option 2 above — otpbox-sdk directly — is the supported path for Puppeteer.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');
OTPBOX_KEY as a repo secret, never in the workflow YAML — see the CI/CD section of the docs for GitHub Actions, GitLab CI, Jenkins and CircleCI examples (the Playwright examples there apply the same way to a Puppeteer job).Idempotency-Key header on POST /api/v1/inboxes if your CI retries flaky network calls, so a retry doesn't create a second inbox — see Idempotency keys.finally block so a failed run doesn't leave either one dangling — though the 15-minute expiry cron cleans up the inbox either way.Ready to try it against your own app? Create a free account
← Back to OTPBox