Mailfornet

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

MethodPathWhat it does
POST/v1/inboxesCreate an inbox. Optional username, domain, ttlMinutes (1–1440, default 60).
GET/v1/inboxes/{address}/messagesList 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/usageRequests used this month, and what's left.
GET/v1/domainsDomains you can create addresses on.
POST/v1/webhooksRegister an endpoint. Returns the signing secret once.
GET/v1/webhooksList your endpoints.
GET/v1/webhooks/{id}/deliveriesThe last 20 attempts, with the status we got back.
POST/v1/webhooks/{id}/testSend 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.

StatusCodeMeans
401missing_api_key, invalid_api_keyNo key, or a key that has been revoked.
402plan_expiredThe subscription lapsed. Renew it.
403api_not_in_planThe plan has no API access.
404inbox_not_found, message_not_foundNot yours, or never existed.
409address_takenSomeone holds that username.
410inbox_expiredThe inbox's lifetime ran out.
429quota_exceededOut 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.

CreditsPricePer 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.

Get an API key

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.