DEV Community

Banoth Sathish
Banoth Sathish

Posted on

How to Test OTP Emails in Playwright Using TempMailGrab

End-to-end tests often get stuck at the same point: the app sends a one-time password, but the test runner cannot safely read a real inbox. TempMailGrab gives each test run a disposable inbox and exposes the parsed OTP through a developer API, so Playwright can complete the full signup or login flow without manual email checking.

Useful links:

Why Use a Disposable Inbox in Tests

Testing OTP email with a shared inbox creates three problems: old messages can be picked up by mistake, parallel test runs collide, and private test credentials end up living in a mailbox. A temporary inbox avoids those issues because each run can create a fresh address, wait for the exact message, extract the OTP, and delete the inbox afterward.

Playwright Example

import { expect, test } from '@playwright/test';

const API_BASE = 'https://tempmailgrab.com/api/v1';
const API_KEY = process.env.TEMPMAILGRAB_API_KEY;

async function createInbox() {
  const response = await fetch(`${API_BASE}/inbox`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ ttl_seconds: 3600 }),
  });

  if (!response.ok) throw new Error(`Inbox creation failed: ${response.status}`);
  return response.json() as Promise<{ id: string; address: string }>;
}

async function waitForOtp(inboxId: string) {
  const deadline = Date.now() + 60_000;

  while (Date.now() < deadline) {
    const response = await fetch(`${API_BASE}/inbox/${inboxId}/messages`, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });

    if (!response.ok) throw new Error(`Message lookup failed: ${response.status}`);
    const data = await response.json() as {
      messages: Array<{ id: string; subject: string | null; extracted_otp: string | null }>;
    };

    const match = data.messages.find((message) => message.extracted_otp);
    if (match?.extracted_otp) return match.extracted_otp;

    await new Promise((resolve) => setTimeout(resolve, 2000));
  }

  throw new Error('OTP email did not arrive in time');
}

test('user can sign up with email OTP', async ({ page }) => {
  const inbox = await createInbox();

  await page.goto('https://your-app.example/signup');
  await page.getByLabel('Email').fill(inbox.address);
  await page.getByRole('button', { name: /continue/i }).click();

  const otp = await waitForOtp(inbox.id);
  await page.getByLabel('Verification code').fill(otp);
  await page.getByRole('button', { name: /verify/i }).click();

  await expect(page.getByText(/welcome/i)).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

CI Notes

Keep the API key in your CI secret store and read it from process.env.TEMPMAILGRAB_API_KEY. Use one inbox per test or per worker when tests run in parallel. For stricter cleanup, call DELETE /inbox/{id} after the flow.

TempMailGrab also publishes a machine-readable OpenAPI contract at https://tempmailgrab.com/api/openapi.json, which can be imported into Postman, generated clients, or internal API catalogs.

Top comments (0)