Testing Auth0 verification emails past the built-in provider

Updated October 17, 2026 · Zyntra

Auth0's built-in email provider is not meant for production and sends at most 10 emails a minute, with no custom templates, so parallel E2E tests outrun it and never see your real template. Configure your own email provider on the staging tenant, test the verification email against a real inbox there, and skip the email in other tests with the Management API.

What the built-in provider allows

  • 10 emails per minute, whatever the email type.
  • No custom email templates.
  • The From address is always [email protected].
  • High bounce rates can reduce or temporarily block sending.

Why that breaks E2E tests

Four parallel workers that each sign up three users send 12 verification emails in well under a minute, which is over the limit, so the tests that wait for the extra emails fail depending on timing. And because templates can't be customised, a test on the built-in provider checks Auth0's default email, not the one your users get. Set up the same external provider on the staging tenant as in production, and both problems go away.

The verification email test

Auth0's verification link points at your tenant's domain (or your custom domain). Pick it by host rather than by position, then open it in the browser.

zyntra.ts
type Email = {
  subject: string;
  body: string;
  html: string | null;
  links: { url: string; text: string }[];
};

// Waits up to 30 s on the server for the first email to the address.
export async function lastEmail(address: string): Promise<Email> {
  const res = await fetch(
    `https://api.zyntra.app/emails/last?email=${encodeURIComponent(address)}`,
    {
      headers: { "X-API-Key": process.env.ZYNTRA_API_KEY! },
      signal: AbortSignal.timeout(45_000),
    },
  );
  if (!res.ok) throw new Error(`No email for ${address}: ${res.status}`);
  return res.json();
}

export const testAddress = (tag: string) =>
  `${process.env.ZYNTRA_TEAM_ID}.${tag}-${Date.now()}@zyntramail.com`;
auth0-verify.spec.ts
import { test, expect } from "@playwright/test";
import { lastEmail, testAddress } from "./zyntra";

test("verification email confirms the address", async ({ page }) => {
  test.setTimeout(90_000);
  const address = testAddress("auth0-signup");

  await page.goto("/");
  await page.getByRole("button", { name: "Sign up" }).click();
  // Universal Login
  await page.getByLabel("Email address").fill(address);
  await page.getByLabel("Password").fill("A-long-test-password-1");
  await page.getByRole("button", { name: "Continue", exact: true }).click();

  const email = await lastEmail(address);
  const auth0Host = process.env.AUTH0_DOMAIN!; // your-tenant.eu.auth0.com or a custom domain
  const verify = email.links.filter((l) => new URL(l.url).host === auth0Host);
  expect(verify).toHaveLength(1);

  await page.goto(verify[0]!.url);
  await expect(page.getByText(/verified/i)).toBeVisible();
});

Skipping the email where it isn't under test

Most tests only need a verified user. Create one through the Management API with email_verified: true, or create an email verification ticket with POST /api/v2/tickets/email-verification (scope create:user_tickets): the response's ticket is the verification URL, which the test can open directly. Either way the run sends fewer emails and stays clear of provider limits.

Questions

Is the built-in provider fine for a development tenant?
For trying flows by hand, yes. For automated tests that run in parallel, the 10-per-minute limit makes them flaky.