Guides / How to test email verification in Playwright

How to test email verification in Playwright

Most sign-up flows send a one-time code or a confirmation link. This guide shows a Playwright test that signs up with a fresh, real address, waits for the email and types the code in, with no fixed sleeps and no shared test inbox.

What you need

  • A free BlinkInbox Developer account and an API key from the dashboard (it starts with bb_live_).
  • The key in your environment as BLINKINBOX_API_KEY, locally and in your CI secrets.
  • A sign-up page that sends an email to the address you enter.

How it works

Each test creates its own inbox through the REST API, so parallel tests never read each other’s mail. After the test submits the form, it calls the /otp endpoint with a timeout. That request is held open until an email with a code or verification link arrives, then returns straight away with the code already extracted.

Because the request returns as soon as the email lands, a test that used to sleep for 30 seconds usually finishes in the time it takes your app to send the email.

The test

tests/signup.spec.ts
import { test, expect } from "@playwright/test";

const API = "https://api.blinkinbox.club/api/v1";
const headers = { Authorization: `Bearer ${process.env.BLINKINBOX_API_KEY}` };

test("new user verifies their email with a code", async ({ page, request }) => {
  test.setTimeout(60_000); // the wait below can take up to 30s

  // 1. A fresh address for this test only
  const inbox = await (await request.post(`${API}/inboxes`, {
    headers, data: { prefix: "pw", ttlMinutes: 15 }
  })).json();

  // 2. Sign up with it
  await page.goto("/signup");
  await page.getByLabel("Email").fill(inbox.address);
  await page.getByRole("button", { name: "Create account" }).click();

  // 3. Wait for the email; the code is already parsed
  const res = await request.get(`${API}/inboxes/${inbox.id}/otp?timeout=30`, { headers });
  expect(res.ok()).toBeTruthy();
  const { otp } = await res.json();

  // 4. Finish the flow
  await page.getByLabel("Verification code").fill(otp);
  await page.getByRole("button", { name: "Verify" }).click();
  await expect(page.getByText("Welcome")).toBeVisible();
});

Waiting for a second email in the same test

If one test triggers several emails (sign-up, then a password reset), pass since with a timestamp taken just before the second action. Only messages received after it count, so the earlier code can’t be picked up by mistake.

second email
const since = new Date().toISOString();
await page.getByRole("button", { name: "Forgot password" }).click();

const reset = await (await request.get(
  `${API}/inboxes/${inbox.id}/otp?timeout=30&since=${encodeURIComponent(since)}`, { headers }
)).json();
await page.goto(reset.verificationLink);

Tips

  • If nothing arrives in time, /otp returns 404 with the code otp_not_found. Raise timeout (up to 60 seconds) if your app is slow to send.
  • Filter with from or subject when your app sends several emails at once, for example from=no-reply@yourapp.com.
  • Short lifetimes (ttlMinutes: 15) keep things tidy. Mail is deleted automatically 24 hours after the inbox expires.
  • Some apps block disposable domains. On Pro and Scale, verify a domain such as test.yourcompany.com and create inboxes on it with {"domain": "test.yourcompany.com"}.

Questions

Do I need to mock the email provider?
No. Your app sends real email through its normal provider, and BlinkInbox receives it like any mailbox would. That tests the template, the provider and the link, not a mock.
Can tests run in parallel?
Yes. Each test creates its own inbox, so there is no shared state. The free plan allows 3 inboxes in any 24 hours; Pro allows 10 and Scale is unlimited, which suits CI.

Try it

A free Developer account gets you an API key and MCP access with 3 inboxes in any 24 hours.

More guides