Use case · one-time codes

Test one-time codes sent by email

When sign-in or sign-up sends a 6-digit code instead of a link, the test reads the email, takes the code from its text with one regular expression and types it in. Zyntra gives the test the email; the test decides what the code looks like.

tests/email-code.spec.ts
// tests/email-code.spec.ts
import { test, expect } from "@playwright/test";
import { newAddress, waitForEmail } from "./zyntra";

const SIX_DIGITS = /\b(\d{6})\b/;

test("sign-in asks for the code sent by email", async ({ page }) => {
  test.setTimeout(90_000);
  const address = newAddress("code");
  // ...create the user with this address first (seed or sign-up)...

  await page.goto("/login");
  await page.getByLabel("Email").fill(address);
  await page.getByLabel("Password").fill("a-long-test-password");
  await page.getByRole("button", { name: "Log in" }).click();

  // The test reads the code from the email body itself.
  const email = await waitForEmail(address, (e) =>
    e.subject.includes("Your sign-in code"),
  );
  const code = email.body.match(SIX_DIGITS)?.[1];
  expect(code, "6-digit code in the email").toBeDefined();

  await page.getByLabel("Code").fill(code!);
  await page.getByRole("button", { name: "Verify" }).click();
  await expect(page.getByText("Dashboard")).toBeVisible();
});
tests/zyntra.ts
// zyntra.ts — read test email from Zyntra
import { randomUUID } from "node:crypto";

const API = "https://api.zyntra.app";
const KEY = process.env.ZYNTRA_API_KEY!;
const TEAM = process.env.ZYNTRA_TEAM_ID!;

export interface Email {
  subject: string;
  body: string; // plain-text part
  html: string | null;
  received_at: string;
  links: { url: string; text: string }[];
}

// A fresh address per test: no mail from earlier runs,
// no clash between parallel workers. Tags use no dots.
export const newAddress = (tag: string) =>
  `${TEAM}.${tag}-${randomUUID().slice(0, 8)}@zyntramail.com`;

// The newest email to `address` that passes `match`.
// The first request waits up to 30 s on the server.
export async function waitForEmail(
  address: string,
  match: (e: Email) => boolean = () => true,
  timeoutMs = 60_000,
): Promise<Email> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const res = await fetch(
      `${API}/emails/last?email=${encodeURIComponent(address)}`,
      { headers: { "X-API-Key": KEY } },
    );
    if (res.ok) {
      const email = (await res.json()) as Email;
      if (match(email)) return email;
    } else if (res.status !== 404) {
      throw new Error(`Zyntra API: ${res.status}`);
    }
    await new Promise((r) => setTimeout(r, 2_000));
  }
  throw new Error(`No matching email for ${address}`);
}

The tests import zyntra.ts, the second file above: about 40 lines of plain fetch, so the same calls work from Cypress, Selenium or any runner. Set ZYNTRA_API_KEY and ZYNTRA_TEAM_ID from your dashboard.

What happens to the email

  1. Your app sends it through its usual email provider, to an address on zyntramail.com.
  2. A real mail server receives it and stores it for your team, with its links pulled out of the HTML and text and its attachments kept.
  3. Your test reads it with one API request. Mail to an address you did not create in the dashboard is deleted after 1 day.

Reading the code

The response has the plain-text body and the html of the email. Match your code format against body; if your email has no plain-text part, match against html instead. Anchor the pattern to the words around the code if the email also contains other numbers, such as an order ID or a year.

If your app sends a link rather than a code, see testing email verification links.

With an AI agent

An agent connected to Zyntra's MCP server can fetch the same email and read the code itself, then type it into the browser. The agent reads the code; Zyntra only hands over the email.

Limits to know about

  • /emails/last waits up to 30 seconds for the first email to an address. Give the test more time than your runner's default.
  • Every API call counts toward your plan's API requests.
  • Mail to these addresses is kept 1 day. Create a permanent inbox in the dashboard to keep it longer.
  • Codes often expire in minutes: read the email in the same test, right after the step that sends it.

Questions

Does Zyntra pull the code out of the email for me?
No. It pulls out links only. Codes come in too many formats, so the test reads them with a pattern that fits your app.
The user gets a code on every sign-in. Will the test read an old one?
Not if it uses a fresh address per test, or passes a match to the helper that checks something only the new email has.

Put the code step in your test

Free plan, no setup: send to any address and read it back.

Start free