Guide: Testing OTP / 2FA
Automate login codes and two-factor checks: trigger the code, wait for it with one call and submit it, with no regular expressions in your tests.
Overview
- Mint a unique address with
inboxes.create(). - Trigger the code (login, sign-up or a 2FA challenge).
- Call
waitForOtp()withsince: inbox.createdAt. - Submit
otp.codeto your application and assert success.
Receiver.email finds codes in the subject or the body (4 to 8 digits, separated or alphanumeric) and tells you which. Waiting from the inbox's createdAt means a code from an earlier run can never be returned.
TypeScript Example
import { ReceiverClient } from "@receiverdotemail/sdk";
const receiver = new ReceiverClient({ apiKey: process.env.RECEIVER_API_KEY });
describe("OTP verification", () => {
it("sends and verifies a 6-digit code", async () => {
const inbox = await receiver.inboxes.create({ prefix: "otp" });
// 1. Trigger the code
await fetch("https://your-app.com/api/auth/otp/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: inbox.address }),
});
// 2. Wait for it (throws ReceiverTimeoutError if it never comes)
const otp = await receiver.emails.waitForOtp({
recipient: inbox.address,
since: inbox.createdAt,
codeLength: 6,
timeout: 60,
});
// 3. Submit it
const res = await fetch("https://your-app.com/api/auth/otp/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: inbox.address, code: otp.code }),
});
expect(res.ok).toBe(true);
});
});Python Example
import httpx
import pytest
from receiverdotemail import ReceiverClient
@pytest.fixture
async def receiver():
async with ReceiverClient() as client: # reads RECEIVER_API_KEY
yield client
@pytest.mark.asyncio
async def test_otp_verification(receiver):
inbox = await receiver.inboxes.create(prefix="otp")
async with httpx.AsyncClient() as http:
# 1. Trigger the code
await http.post("https://your-app.com/api/auth/otp/send", json={"email": inbox.address})
# 2. Wait for it (raises ReceiverTimeoutError if it never comes)
otp = await receiver.emails.wait_for_otp(
recipient=inbox.address,
since=inbox.created_at,
code_length=6,
timeout=60,
)
# 3. Submit it
res = await http.post(
"https://your-app.com/api/auth/otp/verify",
json={"email": inbox.address, "code": otp.code},
)
assert res.status_code == 200Resending a Code
To test "Resend code", wait again from the first email's receivedAt. Only the new email can match, and its code is different from the first:
const first = await receiver.emails.waitForOtp({ recipient: inbox.address, since: inbox.createdAt });
await app.resendCode(inbox.address);
const second = await receiver.emails.waitForOtp({
recipient: inbox.address,
since: first.email.receivedAt,
});
expect(second.code).not.toBe(first.code);When an Email Has Several Codes
otp.code is the newest code found. otp.candidates lists every code in the matching emails, newest first, each with its source and email. Use codeLength to skip numbers of the wrong length, such as a year or an order number, or read candidates to choose yourself.
Tips
- Short-lived codes: submit the code as soon as it arrives; long-polling returns within about two seconds of delivery.
- Other formats: codes with separators (384-729) and alphanumeric codes (XK7F9P) are found too. For anything else, use
waitFor()and reademail.textBody. - No email at all: a
ReceiverTimeoutErrormeans nothing reached the address. Check that your app sends to the exactinbox.address.
Next Steps
- Testing Password Resets: links instead of codes
- CI/CD Integration: run it in your pipeline
- Concepts: how
sinceworks