OTP testing without the flaky waits.
Skip the 10-second sleep and the production SMS bill. catchotp gives Cypress, Playwright, Jest, and pytest a real waiter that resolves the moment the code lands — typically under 700ms.
- Real inboxes on a real domain
- p95 under 1.2s
- Works with any test runner
The problem with testing OTP flows
Every signup flow with email or SMS verification ends up with the same broken pattern in the test suite: the test fires the signup, then sleeps for ten seconds, then checks a manually-managed inbox, then often retries the whole thing because the email landed two seconds late and the assertion ran first. The test is slow, brittle, and tells you nothing the next morning when it fails in CI.
The honest reason these tests look this way is that there has not been a clean primitive for "give me an inbox, then resolve when the OTP arrives." Teams either skip the verification step entirely (and ship bugs that only show up in production), point the signup form at a personal Gmail (and watch the inbox fill with junk), or pay $500-2,000 a month for an enterprise platform that bundles the primitive inside fifty other features.
The hidden cost is also real: tests that send through your production SMS or transactional email provider rack up per-message fees on every CI run, and a flaky parallel test suite can quietly burn a few hundred dollars a month before anyone notices.
How catchotp solves it
Disposable inboxes per test
Create an inbox in under 100ms, scoped to a single test or test run. TTL them automatically — no cleanup script, no shared state between parallel jobs.
Real-time waiters, not polling
waitForOtp() holds an
HTTP long-poll until the matching message arrives, then resolves with the parsed code.
No sleep, no jitter, no retries.
OTP auto-extracted
We parse the code out of the email body using a regex that matches the common patterns (code, OTP, verification) — and you can pass your own pattern for edge cases.
Without catchotp vs with catchotp
| Concern | Without catchotp | With catchotp |
|---|---|---|
| Wait pattern | sleep(10), hope, retry | await waitForOtp() resolves on arrival |
| Per-test isolation | Shared mailbox | Ephemeral inbox per test |
| CI cost per run | $0.01-0.05 in SMS/email fees | $0 |
| OTP parsing | Custom regex per service | Auto-extracted, override supported |
| Setup time | 2-3 days to wire up | 5 minutes |
A complete Playwright example
Here is the same test in three runners. Each one creates an ephemeral inbox, drives the signup form, blocks on the OTP, and asserts the post-login state.
import { test, expect } from '@playwright/test';
import { CatchOTP } from '@catchotp/sdk';
const otp = new CatchOTP({ apiKey: process.env.CATCHOTP_KEY! });
test('user can complete email OTP signup', async ({ page }) => {
// 1. Spin up an ephemeral inbox scoped to this test
const inbox = await otp.inboxes.create({ mode: 'ephemeral', ttlMinutes: 10 });
// 2. Drive the signup form with the burner address
await page.goto('https://example.com/signup');
await page.getByLabel('Email').fill(inbox.address);
await page.getByRole('button', { name: 'Continue' }).click();
// 3. Block until the verification email lands; resolve to the parsed OTP
const code = await otp.inboxes.waitForOtp(inbox.id, { timeoutSeconds: 30 });
// 4. Submit it and assert the post-login state
await page.getByLabel('Verification code').fill(code);
await page.getByRole('button', { name: 'Verify' }).click();
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
}); OTP testing FAQ
How fast does waitForOtp resolve?
Can I use my real test SMTP server?
What happens if the OTP regex misses?
Does it work in CI behind a corporate proxy?
How much does it cost per test run?
Catch your first OTP in 60 seconds.
Free tier includes 5 inboxes and 1,000 messages a month — enough for most CI pipelines.