MailFlatDocs
Documentation/Authentication/API Keys & Auth

Authentication & API Keys

How to authenticate programmatic access to your inbox infrastructure.

Every key has two axes

A key answers two separate questions, and you choose both when you create it. Where it reaches is fixed by the kind of key. What it may do there is a set of permissions you tick.
Account key Inbox key
Looks likemf_live_…mf_sk_…
ReachesAll inboxes in your accountOne single inbox
HeaderX-API-Key: mf_live_…X-API-Key: mf_sk_…
How you get oneAgents → API keys → New keyOpen the inbox → 🔑 in the header
Can create inboxes?Only with inbox:manage❌ never, it has no target outside its own inbox
Best forYour own agents and CI that spin up many inboxesSharing just one inbox with a third party
The first axis: how far a key reaches. Permissions are the second axis and apply inside that reach.
Inboxes are born without a key
Creating an inbox no longer mints a key for it. The inbox exists, and the 🔑 button issues a key when you actually want one. That is why POST /api/v1/inboxes answers with api_key: null.

Permissions: what the key may do

PermissionIn the dialogWhat it unlocks
inbox:readRead messagesList and open mail, download attachments, mark as read
email:sendSend mailSend from your inboxes
message:writeDelete messagesDelete a single message, not the inbox itself
inbox:manageCreate and delete inboxesIncludes burn. Deleting an inbox cannot be undone
fullFull accessEverything above, plus any permission added later
A new key can only read
The default is inbox:read and nothing else, because the safest key is the narrowest one. A key that only reads cannot open an inbox, so the classic agent flow fails on its first call until you also tick inbox:manage.
full is a wildcard, not a fourth checkbox
Ticking all four boxes and choosing full grant the same power today. They part ways tomorrow: full also covers permissions we have not written yet, while four ticks stay exactly four. Pick full only when you mean the wildcard.

Which boxes does an agent need?

The signup flow in our quickstarts opens an inbox, polls for the code, then throws the inbox away. That is two permissions, not one.
The callPermission it needs
POST /api/v1/inboxesinbox:manage
GET /api/v1/inboxesinbox:read
GET /api/v1/inboxes/{addr}/latestinbox:read
GET /api/v1/inboxes/{addr}/messagesinbox:read
POST /api/v1/inboxes/{addr}/sendemail:send
DELETE /api/v1/inboxes/{addr}/messages/{id}message:write
DELETE /api/v1/inboxes/{addr}inbox:manage
POST /api/v1/inboxes/{addr}/burninbox:manage
Missing a permission returns 403, and says which one
The error names the exact scope it wanted, for example This API key is missing the required scope: inbox:manage. You do not have to guess. Add the box in Agents → API keys and issue a new key.

How long the key lives

Every key gets a lifetime when you create it: never expires, 30 days, 90 days or 1 year.
An expired key is refused like an unknown one, with 401. It is not deleted, so it still shows in your inventory and you can see when it lapsed. Nothing rolls it over for you: a CI job holding an expiring key starts failing on the day it lapses, which is the point of choosing a lifetime.

Auth, three ways

UI / session: Authorization: Bearer <token>, from login/signup. Full account in the browser.
Account key: X-API-Key: mf_live_…, every inbox the account owns (Agents → API keys).
Inbox key: X-API-Key: mf_sk_…, one inbox only (🔑 in the inbox header).
Issuing a key always needs the session token. An API key cannot mint another API key, however wide its permissions are, so a leaked narrow key cannot widen itself.

Creating a key from code

Both endpoints take a session token, and both answer with the key in full exactly once.
# an account key that can run the whole signup flow
curl -X POST https://mailflat.net/api/keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "label": "ci", "scopes": ["inbox:read", "inbox:manage"], "expires_in_days": 90 }'
# → { "key": "mf_live_…", "scopes": "inbox:read,inbox:manage", "expires_at": … }
# a key for one inbox, read only
curl -X POST https://mailflat.net/api/inboxes/shop@xxxx.mailflat.net/key \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "scopes": ["inbox:read"] }'
# → { "api_key": "mf_sk_…", "scopes": "inbox:read" }
One key per inbox
If the inbox already has a key, this call is refused rather than silently replacing it. Overwriting would stop whatever is using the old key without a word.

Example: Account key (manage many inboxes)

Classic agent flow: create an inbox, sign up somewhere, poll for the OTP. Needs inbox:manage for step 1 and inbox:read for step 3.
# 1) create a fresh inbox
curl -X POST https://mailflat.net/api/v1/inboxes \
-H "X-API-Key: mf_live_abc123…"
# → { "address": "agent-7f3a@xxxx.mailflat.net", "api_key": null }
# 2) list every inbox the account owns
curl https://mailflat.net/api/v1/inboxes -H "X-API-Key: mf_live_abc123…"
# 3) poll the latest mail (OTP auto-extracted)
curl https://mailflat.net/api/v1/inboxes/agent-7f3a@xxxx.mailflat.net/latest \
-H "X-API-Key: mf_live_abc123…"
# → { "otp": "482913", "subject": "Your code", … }

Example: Inbox key (share just one inbox)

Open the inbox, click 🔑 in the header to issue its key. The key works on both API surfaces, and on both it sees only that inbox.
# read messages of THIS inbox only
curl https://mailflat.net/api/inboxes/you@xxxx.mailflat.net/emails \
-H "X-API-Key: mf_sk_9d2f8a…"
# the v1 surface works too, and lists exactly one inbox
curl https://mailflat.net/api/v1/inboxes -H "X-API-Key: mf_sk_9d2f8a…"
# asking for another inbox → 403
curl https://mailflat.net/api/inboxes/someone-else@…/emails \
-H "X-API-Key: mf_sk_9d2f8a…" # ✗ denied
The reverse mix is the one that bites
An account key on the per-inbox paths (/api/inboxes/{addr}/…) is refused with 403. Those paths take a session token or that inbox's own key. An inbox key on /api/v1 is fine.