Skip to content
catchotp
Use case · OTP testing

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
OTP testing illustration: six-digit code input next to a passing Playwright test run

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();
});
Save your CATCHOTP_KEY as a CI secret. The SDK uses outbound HTTPS only.

OTP testing FAQ

How fast does waitForOtp resolve?
p50 latency from email send to waiter resolution is around 600ms. p95 is under 1.2s. The waiter holds an HTTP long-poll until the matching message is parsed; you do not have to retry.
Can I use my real test SMTP server?
You can, but you lose two things: per-test isolation and OTP extraction. catchotp gives every test its own inbox on a real internet-facing domain, so you exercise the same DNS, MX, and TLS path your production users do.
What happens if the OTP regex misses?
You can pass a custom pattern: waitForOtp(inboxId, { pattern: /code:\s*(\d{6})/ }). The default regex matches 4-8 digit codes after common prefixes (code, OTP, verification). Misses are rare but easy to override.
Does it work in CI behind a corporate proxy?
Yes. The SDK uses outbound HTTPS to api.catchotp.com:443. No inbound ports, no SMTP, nothing your CI provider needs to whitelist beyond standard egress.
How much does it cost per test run?
Free tier covers 1,000 messages and 5 inboxes per month — enough for most CI pipelines. Pro at $29/mo lifts that to 50k messages and 50 inboxes. No per-test billing.

Catch your first OTP in 60 seconds.

Free tier includes 5 inboxes and 1,000 messages a month — enough for most CI pipelines.