DEV Community

MD ANWAR JAHID
MD ANWAR JAHID

Posted on Originally published at tmpmailer.com

How to test signup and OTP flows with a disposable email API

How to test signup and OTP flows with a disposable email API

Every product with an email signup eventually gets the same bug report: "the verification code never arrived." Reproducing it locally means either using a real inbox (and drowning in test mail) or mocking the mailer (and never testing the actual delivery path).

A disposable email API gives you a third option: a real inbox with a real address that you can create, read and throw away from code. In this post I'll wire one up in Node.js and turn an email verification step into a deterministic test.

What we are building

The flow is small:

  1. Register a throwaway API identity.
  2. Ask for a fresh inbox.
  3. Submit the signup form with that address.
  4. Poll the inbox until the verification mail arrives.
  5. Extract the code, finish the signup, delete the inbox.

The examples below use TmpMailer's public v2 API (https://tmpmailer.com/api/v2). It is free to use with rate limits, and the same pattern works with any provider that exposes create/read endpoints.

1. Register a test identity

The API issues a bearer token tied to a device identifier. Generate both per test run so runs never collide.

import { randomUUID } from 'node:crypto';

const BASE = 'https://tmpmailer.com/api/v2';

const deviceId = `ci-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const devicePassword = randomUUID() + randomUUID(); // must be 16+ chars

const register = await fetch(`${BASE}/auth/register`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: JSON.stringify({ device_id: deviceId, device_password: devicePassword }),
}).then((r) => r.json());

if (!register.success) throw new Error(register.message);
const token = register.data.token;
💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน 💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน

Store the token in memory or your CI secret manager. Do not commit it, and do not reuse one identity across parallel jobs.

2. Create a disposable inbox

One authenticated call returns an address that is ready to receive mail immediately.

const created = await fetch(`${BASE}/emails`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
}).then((r) => r.json());

const email = created.data.email; // e.g. nabhua@tmpmailer.com
💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน 💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน

If your test needs a predictable local part, the same API has a custom-address endpoint, but random addresses are safer in CI because they never collide with a previous run.

3. Submit the signup form

This part is your own application, so keep the helper generic. The only rule is that the address must be the one you just created:

await page.fill('input[name="email"]', email);
await page.click('button[type="submit"]');
💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน 💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน

If you are testing an API instead of a browser, post to your own signup endpoint the same way. The inbox does not care how the mail was triggered.

4. Wait for the message and extract the code

Email delivery is asynchronous, so polling is unavoidable. Use a deadline and a fixed interval, not a fixed sleep:

async function waitForOtp(email, token, { timeoutMs = 90_000, intervalMs = 3_000 } = {}) {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const res = await fetch(`${BASE}/messages/${encodeURIComponent(email)}`, {
      headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
    }).then((r) => r.json());

    for (const message of res.data?.messages ?? []) {
      const text = [message.subject, message.preview, message.plain_text]
        .filter(Boolean)
        .join('\n');

      const match = text.match(/\b(\d{6})\b/);
      if (match) return match[1];
    }

    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error('Verification email did not arrive within 90s');
}
💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน 💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน

Two details that save flaky builds:

  • Match on a six-digit group, not on the first number in the message. Marketing footers love phone numbers and order IDs.
  • Return the first match and stop polling. The message list is not ordered the way you expect when two mails arrive together.

5. Clean up

Delete the inbox when the test finishes, even on failure:

try {
  const otp = await waitForOtp(email, token);
  await submitOtp(otp);
} finally {
  await fetch(`${BASE}/emails/${encodeURIComponent(email)}`, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${token}` },
  });
}
💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน 💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน

Disposable inboxes expire on their own, but deleting explicitly keeps your test data and the provider's shared domains tidy.

The cURL version

When you are debugging the flow by hand, this is enough:

# 1. create an inbox (after registering and exporting TOKEN)
curl -s -X POST https://tmpmailer.com/api/v2/emails \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

# 2. read the inbox
curl -s "https://tmpmailer.com/api/v2/messages/$EMAIL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"
💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน 💬 ชาวเน็ตแห่แชร์: ringkubd/how to test signup and otp flows with a disposable email api ogo - ประเด็นร้อน

What this approach does and does not test

It does test the real path: your form, your backend, your SMTP provider, the provider's inbound filtering and the message rendering. That is exactly where "code never arrived" bugs live.

It does not replace a production mailbox. Disposable domains are blocked by some services, messages expire, and you should never attach billing, recovery or long-lived account data to an address that disappears. Keep disposable inboxes for tests, trials and one-off signups.

Also respect the service you are testing. Creating accounts in bulk to abuse trials or bypass limits is not a testing strategy, and most terms of service prohibit it.

CI checklist

  • Generate a unique identity per run.
  • Poll with a 60–90 second deadline and a 3–5 second interval.
  • Retry the whole test once on timeout before failing the build.
  • Delete the inbox in a finally block.
  • Never log the bearer token.

That is the whole pattern. The first time it catches a real delivery failure instead of a mocked success, it pays for itself.


TmpMailer is a free temporary email service with a documented API for signup and verification testing. The API used above is described in more detail on the TmpMailer developer guide.

Top comments (0)