Email API
Create a disposable inbox from your code, then read the mail that lands in it. Built for the test that signs up, waits for a verification code and types it back in.
Looking for an inbox to read in your browser? That's the free temp mail, or a permanent address plan. This page is the API for software.
What it's for
Every signup flow needs an email address, so every test of that flow needs one too — a fresh one each time, or the second run collides with the first. Teams usually end up with a catch-all mailbox, an IMAP client in the test suite, and a slow, flaky test that nobody trusts.
This replaces all of that with two HTTP calls: ask for an address, then read what arrives at it.
A complete example
Signing up and pulling the verification code back out, in the order a test would do it:
curl -X POST https://api.mailfornet.com/v1/inboxes \
-H "Authorization: Bearer mf_live_your_key" \
-H "Content-Type: application/json" \
-d '{"ttlMinutes": 30}'
{"address":"k3f9xq2m@mailfornet.com","expiresAt":"2026-09-19T18:24:00Z"}
Sign up on your app with that address, then ask for the mail. Add wait and the request
holds open until something arrives, so you never write a polling loop:
curl "https://api.mailfornet.com/v1/inboxes/k3f9xq2m@mailfornet.com/messages?wait=30" \
-H "Authorization: Bearer mf_live_your_key"
{"address":"k3f9xq2m@mailfornet.com","messages":[
{"id":"m_8821","from":"noreply@yourapp.com","subject":"Confirm your email","receivedAt":"..."}
]}
Then read the message itself and pull the code out of it:
curl "https://api.mailfornet.com/v1/messages/m_8821?address=k3f9xq2m@mailfornet.com" \
-H "Authorization: Bearer mf_live_your_key"
In a test
const api = (path, init) => fetch(`https://api.mailfornet.com/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.MAILFORNET_KEY}`, ...init?.headers },
}).then((r) => r.json());
// A fresh address for this test run
const { address } = await api('/inboxes', { method: 'POST' });
await signUp(address);
// One call: it returns as soon as the mail lands, or after 30s if it never does
const { messages } = await api(`/inboxes/${address}/messages?wait=30`);
const mail = await api(`/messages/${messages[0].id}?address=${address}`);
await enterCode(mail.text.match(/\d{6}/)[0]);
Endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /v1/inboxes | Create an inbox. Optional username, domain, ttlMinutes (1–1440, default 60). |
| GET | /v1/inboxes/{address}/messages | List what has arrived. Optional limit (1–100), wait (0–60 seconds), since (unix ms), from, subject. |
| GET | /v1/messages/{id}?address= | One message, with its text and HTML. |
| DELETE | /v1/inboxes/{address} | Delete an inbox and its mail now, without waiting for it to expire. |
| GET | /v1/usage | Requests used this month, and what's left. |
| GET | /v1/domains | Domains you can create addresses on. |
| POST | /v1/webhooks | Register an endpoint. Returns the signing secret once. |
| GET | /v1/webhooks | List your endpoints. |
| GET | /v1/webhooks/{id}/deliveries | The last 20 attempts, with the status we got back. |
| POST | /v1/webhooks/{id}/test | Send a sample event, to check your endpoint and signature code. |
| DELETE | /v1/webhooks/{id} | Remove an endpoint. |
Waiting for one particular email
An inbox in a test often receives more than the message you care about. from and
subject match on a substring, and combine with wait, so you can hold out for
exactly the mail your test is about rather than whatever arrived first:
curl "https://api.mailfornet.com/v1/inboxes/$ADDRESS/messages?wait=30&subject=verify" \
-H "Authorization: Bearer mf_live_your_key"
Other languages
Python:
import os, requests
API = "https://api.mailfornet.com/v1"
headers = {"Authorization": f"Bearer {os.environ['MAILFORNET_KEY']}"}
address = requests.post(f"{API}/inboxes", headers=headers).json()["address"]
sign_up(address)
# Returns as soon as the mail lands
mail = requests.get(
f"{API}/inboxes/{address}/messages",
params={"wait": 30, "subject": "verify"},
headers=headers,
).json()["messages"][0]
body = requests.get(f"{API}/messages/{mail['id']}", params={"address": address}, headers=headers).json()
code = re.search(r"\d{6}", body["text"]).group()
PHP:
$key = getenv('MAILFORNET_KEY');
$ctx = ['http' => ['header' => "Authorization: Bearer $key\r\n"]];
$inbox = json_decode(file_get_contents(
'https://api.mailfornet.com/v1/inboxes',
false,
stream_context_create(['http' => ['method' => 'POST', 'header' => "Authorization: Bearer $key\r\n"]])
), true);
$messages = json_decode(file_get_contents(
"https://api.mailfornet.com/v1/inboxes/{$inbox['address']}/messages?wait=30",
false,
stream_context_create($ctx)
), true);
Webhooks
Rather than asking us whether mail has arrived, you can be told. Register an https endpoint and we POST to it the moment a message reaches any of your addresses:
{
"event": "message.received",
"createdAt": "2026-09-19T18:05:26Z",
"data": {
"address": "k3f9xq2m@mailfornet.com",
"from": "noreply@yourapp.com",
"subject": "Confirm your email",
"intro": "Your code is 493021",
"receivedAt": "2026-09-19T18:05:26Z"
}
}
Three headers come with it: Mailfornet-Event, Mailfornet-Timestamp and
Mailfornet-Signature. The signature is an HMAC-SHA256 of
{timestamp}.{raw body} using the secret you were given when you registered the endpoint.
Verify it before trusting the request — otherwise anyone who guesses your URL can forge one:
import crypto from 'node:crypto';
const expected = crypto
.createHmac('sha256', process.env.MAILFORNET_WEBHOOK_SECRET)
.update(`${req.headers['mailfornet-timestamp']}.${rawBody}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['mailfornet-signature']))) {
return res.status(400).end();
}
Reply with a 2xx. Anything else counts as a failure, and an endpoint that fails 20 times in a row is
switched off — check /v1/webhooks/{id}/deliveries to see what we got back from yours.
You don't have to send yourself a real email to check any of this. Send test in
your account (or POST /v1/webhooks/{id}/test) delivers a sample
webhook.test event, signed exactly like a real one, and tells you what your endpoint
answered.
Retrying safely
Send an Idempotency-Key header on a POST and a repeat of that request returns the
original response instead of doing the work twice. A runner that times out and retries then ends up
with one inbox, not two:
curl -X POST https://api.mailfornet.com/v1/inboxes \
-H "Authorization: Bearer mf_live_your_key" \
-H "Idempotency-Key: signup-test-run-8821"
A replayed response carries Idempotent-Replayed: true.
OpenAPI
The whole API is described at /openapi.json (OpenAPI 3.1). Point your generator at it and get a typed client in your own language, rather than writing one from this page.
Authentication
Every request carries your key as a bearer token:
Authorization: Bearer mf_live_...
Create keys in your account. A key is shown once, when you create it, and only a hash of it is stored here — so if you lose it, revoke it and make another. Keep keys out of your repository and in your CI secrets.
Limits
Every response tells you where you stand, so a client can slow down before it is throttled:
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9863
X-RateLimit-Reset: 1790812800
Requests are counted per calendar month and reset on the 1st. Inboxes you create belong to your account and never count against your permanent-address allowance.
Errors
Every response carries a Mailfornet-Request-Id, and every failure repeats it in the body.
Quote it to support and we can find the exact request.
{
"error": {
"type": "invalid_request_error",
"code": "address_taken",
"message": "That address is already taken",
"docUrl": "https://mailfornet.com/api"
},
"requestId": "req_dLD-T3vzY1TUJ8kX"
}
Branch on code, not on the message — the wording may change, the code will not.
| Status | Code | Means |
|---|---|---|
| 401 | missing_api_key, invalid_api_key | No key, or a key that has been revoked. |
| 402 | plan_expired | The subscription lapsed. Renew it. |
| 403 | api_not_in_plan | The plan has no API access. |
| 404 | inbox_not_found, message_not_found | Not yours, or never existed. |
| 409 | address_taken | Someone holds that username. |
| 410 | inbox_expired | The inbox's lifetime ran out. |
| 429 | quota_exceeded | Out of requests for the month. |
Pricing
Every plan includes unlimited inboxes from the API, long-polling, webhooks and usernames you choose. Inboxes you create expire on their own and never count against a plan's permanent addresses.
After that, pay as you go
When the month's 10,000 are gone, requests come out of credits you have bought. Credits never expire, and nothing is ever charged automatically — a test loop that runs away overnight cannot produce a bill you did not agree to. It simply stops.
| Credits | Price | Per 1,000 |
|---|---|---|
| 10,000 requests | $25 | $2.50 |
| 50,000 requests | $99 | $1.98 |
| 200,000 requests | $299 | $1.50 |
Buy them from your account. While credits are paying for your requests, every
response carries X-Credits-Remaining, so your own code can tell when it is running low.
Need far more than this? Tell us what you need and we'll quote it.
Fair use
The API is for testing your own software. It isn't for creating accounts in bulk on services you don't own, and our Terms of Service apply to it exactly as they do to the rest of Mailfornet.