Defaults below are from Better Auth’s documentation on 17 October 2026. Check it for current details.
What you send, and when
| Your function | Notes | |
|---|---|---|
| Address verification | emailVerification.sendVerificationEmail |
Sent at sign-up with sendOnSignUp: true |
| Password reset | emailAndPassword.sendResetPassword |
A link to set a new password |
| Magic link | sendMagicLink (magic link plugin) |
Expires after 300 seconds by default; works once |
| Email OTP | sendVerificationOTP (email OTP plugin) |
6 digits, 300 seconds, 3 attempts by default; for sign-in, verification or a password reset |
Better Auth’s docs suggest not awaiting the send, to avoid timing attacks. On serverless platforms that means using waitUntil or similar, or the function can end before the email goes.
Test it by hand
- Create an address in Proxy Mail QA, with a persona and password if your form asks.
- Sign up in your app with it.
- The link or code appears on the address’s row within seconds, with a countdown when the email says how long it lasts.
Try the edges too: sign in before verifying (refused with requireEmailVerification), open a magic link twice (the second time fails), and wait past five minutes.
Automate it
import { test, expect } from 'proxymail-qa/playwright';
test('email OTP sign-in works', async ({ page, qa }) => {
const { address } = await qa.createAddress();
const requested = new Date();
await page.goto('/sign-in');
await page.getByLabel('Email').fill(address);
await page.getByRole('button', { name: 'Send code' }).click();
const { value } = await qa.waitForCode(address, { since: requested });
await page.getByLabel('Code').fill(value);
await expect(page).toHaveURL(/dashboard/);
});
For links, use qa.waitForLink(address, { type: 'verify', since }) or type: 'magic_signin' and open them. More in Test sign-up emails in Playwright.