Email Testing API
Create a real, isolated inbox for every test run with one HTTP call. Receive the verification email, and read the one-time code straight out of a JSON field. No IMAP server, no shared QA mailbox, no flaky regex over an HTML template.
Your Temporary Email Address
Waiting for mail...
- No signup
- 24-hour inbox
- OTP auto-detect
- Zero tracking
- Android · Chrome · Firefox
- Developer API
The problem with testing email flows
Almost every product has a flow that only works if an email actually arrives: sign-up confirmation, password reset, magic link, two-factor code, invitation acceptance. These are the flows most likely to break in production and the least likely to be covered by an automated test — because testing them properly is awkward.
The usual workarounds share one failure mode. A shared QA mailbox makes parallel runs compete for messages: test A's confirmation email gets claimed by test B, the wrong code is submitted, and the failure is non-deterministic. Mocking the mail transport tests your code but not your templates, your ESP configuration, or deliverability — the parts that actually break. Reading a real inbox over IMAP means credentials in CI, connection limits, and a mailbox that fills up and needs cleaning.
An email testing API removes the shared resource. Each test creates its own inbox, uses it, and lets it expire. There is nothing to reset between runs and nothing to contend over.
How it works
- Create an inbox.
POST /api/v1/inboxreturns a real, working address in milliseconds. - Poll for the message.
GET /api/v1/inbox/{id}/messagesreturns messages newest-first, each already carryingextracted_otpandextracted_links. - Or subscribe. Register a webhook and the parsed message is POSTed to you instead, signed with HMAC-SHA256.
The code and the verification links are extracted at ingestion, on the server, before you ever see the message. That removes the most fragile part of an email test: you are not maintaining a regex over an HTML body that breaks the next time someone edits the template.
Create an inbox
curl -X POST https://tempmailgrab.com/api/v1/inbox \
-H "Authorization: Bearer $TMG_API_KEY" \
-H "content-type: application/json" \
-d '{"prefix":"ci-run-8412","ttl_seconds":3600}'
# -> {
# "id": "8f14e45f-ceea-467a-9f0b-2c1d0d5e7a11",
# "address": "ci-run-8412@tempmailgrab.com",
# "created_at": 1787834622,
# "expires_at": 1787838222
# }
prefix and ttl_seconds are both optional. Omit the prefix and you get a random 12-character local part; omit the TTL and the inbox lives 24 hours. TTL accepts 600–259200 seconds (10 minutes to 72 hours), so a CI inbox can be scoped to the length of the job that owns it.
Read the message and the code
curl https://tempmailgrab.com/api/v1/inbox/$INBOX_ID/messages \
-H "Authorization: Bearer $TMG_API_KEY"
# -> {
# "address": "ci-run-8412@tempmailgrab.com",
# "messages": [{
# "id": "aa09e0aa-1359-4fb8-a801-e1b9bc02424f",
# "sender": "no-reply@yourapp.com",
# "subject": "Your verification code",
# "snippet": "Your code is 448213 ...",
# "extracted_otp": "448213",
# "created_at": 1787834649
# }]
# }
Fetch a single message with GET /api/v1/inbox/{id}/messages/{messageId} for the full record: text_body, a sanitized html_body, extracted_links, raw_headers, and signed download URLs for any attachments.
Playwright
A fixture that hands each test its own address, plus a helper that waits for the code:
import { test as base, expect } from '@playwright/test';
const API = 'https://tempmailgrab.com/api/v1';
const headers = { authorization: `Bearer ${process.env.TMG_API_KEY}` };
async function waitForOtp(inboxId, timeoutMs = 30000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(`${API}/inbox/${inboxId}/messages`, { headers });
const { messages } = await res.json();
const hit = messages.find((m) => m.extracted_otp);
if (hit) return hit.extracted_otp;
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error('No OTP arrived within the timeout');
}
export const test = base.extend({
inbox: async ({}, use) => {
const res = await fetch(`${API}/inbox`, { method: 'POST', headers });
await use(await res.json()); // inboxes expire on their own - no teardown
},
});
test('a new user can verify their email', async ({ page, inbox }) => {
await page.goto('/signup');
await page.getByLabel('Email').fill(inbox.address);
await page.getByRole('button', { name: 'Create account' }).click();
const code = await waitForOtp(inbox.id);
await page.getByLabel('Verification code').fill(code);
await page.getByRole('button', { name: 'Verify' }).click();
await expect(page.getByText('Welcome')).toBeVisible();
});
Because the fixture is per-test, Playwright's parallel workers cannot collide: worker 1 and worker 7 hold different addresses and never see each other's mail. There is no afterEach, because the inbox deletes itself.
Cypress
// cypress/support/commands.js
Cypress.Commands.add('createInbox', () =>
cy.request({
method: 'POST',
url: 'https://tempmailgrab.com/api/v1/inbox',
headers: { authorization: `Bearer ${Cypress.env('TMG_API_KEY')}` },
}).its('body'),
);
Cypress.Commands.add('waitForOtp', (inboxId) =>
cy.request({
url: `https://tempmailgrab.com/api/v1/inbox/${inboxId}/messages`,
headers: { authorization: `Bearer ${Cypress.env('TMG_API_KEY')}` },
}).then(({ body }) => {
const hit = body.messages.find((m) => m.extracted_otp);
if (hit) return hit.extracted_otp;
cy.wait(1000);
return cy.waitForOtp(inboxId);
}),
);
// cypress/e2e/signup.cy.js
it('verifies a new account', () => {
cy.createInbox().then((inbox) => {
cy.visit('/signup');
cy.get('[name=email]').type(inbox.address);
cy.contains('Create account').click();
cy.waitForOtp(inbox.id).then((code) => {
cy.get('[name=code]').type(code);
cy.contains('Verify').click();
cy.contains('Welcome').should('be.visible');
});
});
});
Webhooks instead of polling
For longer flows — a nightly job, a staging environment, an inbound-mail integration you are building — register a webhook and skip the poll loop:
curl -X POST https://tempmailgrab.com/api/v1/webhooks \
-H "Authorization: Bearer $TMG_API_KEY" \
-H "content-type: application/json" \
-d '{"url":"https://your-ci.example.com/hooks/email"}'
# -> { "id": "...", "secret": "whsec_..." }
Every message to any of your inboxes is then POSTed to that URL as parsed JSON with an X-TMG-Signature header containing the HMAC-SHA256 of the body under your secret. Verify it before trusting the payload. Account-level webhooks fire for every inbox the key owns; per-inbox webhooks (POST /api/v1/inbox/{id}/webhook) scope delivery to a single address.
Testing the test harness
Wiring up email tests has a chicken-and-egg problem: until your application sends its first message you cannot tell whether your polling code works or your assertions are simply never reached. POST /api/v1/inbox/{id}/test-message injects a synthetic message — with a real, extractable code — so you can prove the harness end to end before pointing it at your app.
curl -X POST https://tempmailgrab.com/api/v1/inbox/$INBOX_ID/test-message \
-H "Authorization: Bearer $TMG_API_KEY" \
-H "content-type: application/json" \
-d '{"subject":"Your code","text":"Your verification code is 448213."}'
# -> { "ok": true, "id": "...", "extracted_otp": "448213" }
Keys, limits and cost
- Authentication:
Authorization: Bearer <key>orX-API-Key: <key>. Create keys in the dashboard. - Rate limit: per key, not per IP — a CI runner behind a shared NAT gets its own budget.
- Isolation: inboxes belong to the account that created them; another key cannot read your mail.
- Cleanup: none. Inboxes and their messages are purged on expiry by a scheduled job.
- Cost: free to start — see pricing for higher limits and custom domains.
Related reading
Automate inboxes with the API
Prefer to script it? Generate disposable inboxes and read auto-parsed OTPs & verification links programmatically — built for QA, test automation, and bots. The same private inboxes, over a clean REST API.
# 1. Create a private inbox
curl -X POST https://tempmailgrab.com/api/v1/inbox \
-H "Authorization: Bearer $TMG_API_KEY"
# → { "id": "inb_…", "address": "a1b2c3d4e5f6@tempmailgrab.com" }
# 2. Poll for mail — the OTP + verification links are auto-extracted
curl https://tempmailgrab.com/api/v1/inbox/$ID/messages \
-H "Authorization: Bearer $TMG_API_KEY"
# → { "messages": [{ "extracted_otp": "123456", "extracted_links": [ … ] }] }Related guides
Frequently Asked Questions
What is an email testing API?
An HTTP API that creates real, disposable inboxes on demand so automated tests can receive and assert on actual email. Instead of mocking your mail transport or sharing one QA mailbox, each test run gets its own address and reads the message back over REST.
How do I test an OTP or verification code automatically?
Create an inbox, use its address in your sign-up flow, then poll GET /api/v1/inbox/{id}/messages. Each message includes an extracted_otp field with the one-time code already parsed out, so you can assert on it directly without writing a regex over the email body.
Does it work with Cypress and Playwright?
Yes — it is a plain REST API, so it works from any test runner. This page includes a Playwright fixture and Cypress custom commands you can copy, and there are full tutorials for both in the blog.
Can parallel tests interfere with each other?
No, provided each test creates its own inbox. That is the main advantage over a shared QA mailbox, where two concurrent tests can claim each other’s verification emails and fail non-deterministically.
Do I need to clean up inboxes after a test run?
No. Every inbox has a TTL (10 minutes to 72 hours, 24 hours by default) and is purged automatically along with its messages, so there is no teardown step and no mailbox that fills up over time.
Can I use webhooks instead of polling?
Yes. Register a webhook and each parsed message is POSTed to your endpoint with an HMAC-SHA256 X-TMG-Signature header. Account-level webhooks cover every inbox the key owns; per-inbox webhooks scope delivery to one address.
Is the email testing API free?
There is a free tier that covers typical CI usage. Higher rate limits, custom domains and priority support are on the paid plan.
Can I test attachments and HTML email?
Yes. Fetching a single message returns the plaintext body, a sanitized HTML body, the raw headers, and signed download URLs for every attachment.