Use case · email verification
Test email verification in your sign-up flow
The test signs up with a fresh address, waits for the confirmation email, opens the link from it and checks the account is confirmed. Without a test inbox this step is usually skipped, and a broken template or a wrong link in production is found by a user who cannot sign up.
// tests/email-verification.spec.ts
import { test, expect } from "@playwright/test";
import { newAddress, waitForEmail } from "./zyntra";
test("sign-up sends a working confirmation link", async ({ page }) => {
test.setTimeout(90_000);
const address = newAddress("verify");
await page.goto("/signup");
await page.getByLabel("Email").fill(address);
await page.getByLabel("Password").fill("a-long-test-password");
await page.getByRole("button", { name: "Sign up" }).click();
const email = await waitForEmail(address);
expect(email.subject).toContain("Confirm your email");
// Links are already pulled out of the HTML and text.
const confirm = email.links.find((l) => l.url.includes("/verify"));
expect(confirm, "confirmation link").toBeDefined();
await page.goto(confirm!.url);
await expect(page.getByText("Your email is confirmed")).toBeVisible();
});// 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
- Your app sends it through its usual email provider, to an address on zyntramail.com.
- 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.
- Your test reads it with one API request. Mail to an address you did not create in the dashboard is deleted after 1 day.
What the test proves
- Sign-up sends an email at all, through your real provider.
- It goes to the address the user typed, with the subject you expect.
- The link in it points to the right environment and confirms the account.
Finding the link
Every email comes with links: each link found in the HTML and the plain text, as { url, text }. Pick the confirmation link by its path or its button text; no HTML parsing in the test. If your app sends a code instead of a link, see testing codes sent by email.
Limits to know about
/emails/lastwaits 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.
Questions
- Do I have to create the inbox before the test signs up?
- No. Any address with your team ID receives mail; the test makes up a new one each run.
- My app's sign-up blocks unknown email domains. Will this work?
- Addresses end in
@zyntramail.com. If staging only accepts certain domains, add this one to the list for your test environment. - Can I check the email's HTML as well?
- Yes. The response has
htmland the plain-textbody, so the test can assert on wording, the user's name or anything else in the message.
Cover email verification in your next test run
Free plan, no setup: send to any address and read it back.
Start free