A temporary email API for testing signup flows
End-to-end tests that cover registration hit the same wall every time: the flow sends a verification email and the test has no inbox to read it from. The usual answers are mocking the mail provider — which stops testing the thing you meant to test — or wiring a real mailbox into CI, which means mailbox credentials in CI.
SpareInbox has two endpoints that need neither. No key, no account, no signup. They are the same two the page itself uses, so nothing here is a side door that can quietly rot.
Create an address
POST https://spareinbox.co/api/mailbox
201 { "address": "k7m2q4x9ab@spareinbox.co" }
429 { "error": "Too many addresses from here. Try again shortly." }
Retry-After: 1358No body and no headers. The address is random, ten characters from an unambiguous alphabet, and yours for as long as you keep reading it.
Read what arrived
GET https://spareinbox.co/api/mailbox/{address}/messages
GET https://spareinbox.co/api/mailbox/{address}/messages?since=2026-08-18T09:12:44.000Z
200 { "messages": [ … ] } newest first
400 { "error": "`since` must be an ISO timestamp." }
404 { "error": "No such mailbox." } expired, or never existedsince is exclusive: pass the receivedAt of the newest message you already hold and you will never be handed it twice. That is what makes a polling loop safe to write.
A 404 is not an error to retry. It means the mailbox is gone — expired, or never created — and polling will never succeed. Treat it as a failed test, not a slow one.
What a message looks like
{
"id": "msg_5f0c…",
"fromAddress": "noreply@example.com",
"fromName": "Example Service",
"subject": "Your verification code",
"receivedAt": "2026-08-18T09:12:44.000Z",
"verificationCode": "731204",
"actionLink": null,
"textBody": "Your verification code is 731204…",
"htmlBody": "<p>…</p>",
"attachmentNames": ["receipt.pdf"]
}verificationCodeandactionLinkare extracted when the message arrives, not when you ask. Either isnullwhen the message did not contain one, which is a real answer rather than a failure — plenty of mail has a link and no code.attachmentNamesis names only. The bytes are discarded at ingest and never stored, so there is nothing to download.htmlBodyis the sender’s markup, untouched. If you render it, sandbox it — it is a stranger’s HTML.
A helper worth copying
export async function waitForCode(address, { timeoutMs = 60_000 } = {}) {
const base = 'https://spareinbox.co/api/mailbox'
const url = `${base}/${encodeURIComponent(address)}/messages`
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const response = await fetch(url)
if (response.status === 404) throw new Error(`${address} no longer exists`)
if (response.ok) {
const { messages } = await response.json()
const found = messages.find((m) => m.verificationCode)
if (found) return found.verificationCode
}
await new Promise((r) => setTimeout(r, 2000))
}
throw new Error(`no code arrived at ${address} within ${timeoutMs}ms`)
}In a Playwright test
import { test, expect } from '@playwright/test'
import { waitForCode } from './spareinbox'
test('a new user can register', async ({ page }) => {
const { address } = await fetch('https://spareinbox.co/api/mailbox', {
method: 'POST',
}).then((r) => r.json())
await page.goto('/signup')
await page.fill('#email', address)
await page.click('#submit')
await page.fill('#code', await waitForCode(address))
await expect(page.getByText('Welcome')).toBeVisible()
})When the flow sends a link instead
// Some services send a confirmation link instead of a code. Follow it with a
// request rather than a browser: the link is single-use, and opening it in the
// page under test can log you in as somebody the test did not create.
const { messages } = await fetch(url).then((r) => r.json())
const link = messages.find((m) => m.actionLink)?.actionLink
if (link) await fetch(link, { redirect: 'follow' })The limits, stated plainly
- 20 addresses per hour, per IP. Over that, creation answers 429 with
Retry-Afterin seconds. Reading is not limited — one address polled hard is fine. If a suite needs more than twenty mailboxes an hour, reuse one: a mailbox holds any number of messages andsincekeeps them apart. - No authentication, and therefore no privacy. Anyone who knows an address can read its mail. Never point a real account at this.
- Messages live 7 days, mailboxes 30 days from the last read. A mailbox your suite uses weekly stays alive on its own.
- No webhooks. Polling is the interface, which is why the helper polls.
- Nothing is sent. This system has no outbound path at all, so it cannot test flows that expect a reply.
- No uptime promise. It is free and it is one person’s project. If a release blocks on it, that is a risk you are choosing knowingly.
- Some signup forms reject the domain. Why that happens — and it will happen to some of the sites you are testing against.
Being a good neighbour
Poll every two seconds or slower. Verification mail takes seconds to arrive and no test is made faster by asking ten times a second — it only makes the limit above something that has to exist for reading too.
If this is load-bearing for you, run your own. Everything here is a Next.js application, a Cloudflare Email Worker and one Postgres schema; the whole receiving path is a few hundred lines, and it will always be more reliable in a place you control.