Skip to main content

Authentication — API keys, scopes, and the Bearer header

Bearer keys, the mk_live_ format, four scopes (send / read / manage / draft), live vs test mode, and rotation. Everything about authenticating a request.

Every request to https://api.mails.ai authenticates with an API key in the standard bearer header:

Authorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

There is no other auth scheme — no query-string key, no x-api-key header. A request with no key, or a malformed one, gets a structured 401 that tells you exactly what went wrong:

{
  "error": {
    "type": "authentication_error",
    "code": "missing_authorization",
    "message": "Missing Authorization header. Use `Authorization: Bearer mk_live_...` or sign in for dashboard access.",
    "param": null,
    "request_id": "req_01KVF46QACE5C361SBYTP6JE0S"
  }
}

Key format

Keys are formatted mk_{mode}_{32 hex chars}, where {mode} is live or test:

  • mk_live_… — sends real email, meters real usage and cost.
  • mk_test_… — exercises the full API shape without sending real mail or incurring cost. Use it in CI and local dev.

The full key is shown exactly once, at creation. mails.ai stores only a SHA-256 hash — it cannot show you the key again, so save it immediately. If you lose it, rotate (mint a new one, revoke the old).

Getting a key

Sign in at app.mails.ai and mint a key under API keys, or create one programmatically once you already hold a key with the manage scope:

curl https://api.mails.ai/v1/api-keys \
  -H "Authorization: Bearer $MAILS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "inbound-worker",
    "scopes": ["read"],
    "mode": "live"
  }'
{
  "id": "key_01J9X2K7Q8...",
  "key": "mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "prefix": "mk_live_xxxx",
  "mode": "live",
  "scopes": ["read"],
  "agent_id": null,
  "name": "inbound-worker",
  "expires_at": null,
  "created_at": "2026-06-19T17:30:00.000Z"
}

A key made this way reaches no further than the key that made it. It gets no scope the maker lacks, the same agent when the maker is bound to one, test mode when the maker is a test key, and an expires_at no later than the maker’s own. Asking for more answers 403 insufficient_scope.

Minting your first key is self-serve — sign in and create one under API keys. No request or approval step.

In a workspace with several people, creating and revoking keys in the dashboard takes the owner or admin role.

Scopes

Every key carries a set of scopes. mails.ai is secure-by-default: a key with an empty scope set can authenticate but is authorized for nothing. The four scopes map cleanly to what a request does:

  • send — send, reply, forward, and create drafts that send.
  • read — list and fetch messages, events, threads, reputation, and usage.
  • manage — create and revoke keys, manage agents and webhooks, edit suppression, cancel or reschedule messages (rescheduling also needs send).
  • draft — list, read, create, edit and delete drafts, but never send or schedule one: a key that should only prepare mail for a person to send.

A key may also be bound to a single agent via agent_id at creation, so a leak is contained to one identity. Such a key reads and acts on that agent’s mail only: another agent’s message, thread, draft, attachment or raw email answers 404, and lists and search hold only its own agent’s mail. Webhooks and labels belong to the whole workspace, so an agent-bound key cannot create, change, delete, test or replay a webhook, or create, rename or delete a label (those calls answer 403).

The same holds for everything else the whole workspace shares. An agent-bound key cannot create an agent, change its own agent’s allowlist, blocklist or send limits, revoke an API key, add, verify or remove a sending domain, allow a suppressed address back or undo that, send an event to the webhooks again, list or revoke connected apps, open the billing portal, or read the workspace’s usage and request log. Each answers 403 insufficient_scope. GET /v1/metrics gives such a key its own agent’s counts.

People signed in to the dashboard act through the same API, with scopes set by their role. Owners and admins hold all four scopes. Members hold send, read and draft, and can also file, label and trash threads, change labels, and cancel a scheduled send. The billing role holds read and can open the billing portal. Those last two abilities are scopes of their own (inbox and billing) that only a dashboard session holds: no API key or connected app can have them.

GET /v1/me echoes the calling key’s scopes, mode, and bound agent — a useful sanity check:

curl https://api.mails.ai/v1/me -H "Authorization: Bearer $MAILS_API_KEY"

Rotation and revocation

Keys never expire unless you set expires_at. To rotate, mint the replacement, deploy it, then revoke the old one:

curl -X DELETE https://api.mails.ai/v1/api-keys/key_01J9X2K7Q8... \
  -H "Authorization: Bearer $MAILS_API_KEY"

Revocation takes effect immediately — the next request with that key gets 401 revoked_api_key. An expired key gets 401 expired_api_key; an unknown key gets 401 invalid_api_key.

Connected apps

An app can also reach your workspace without a key: you sign in to mails.ai in your browser and approve it. The mails CLI connects this way with mails login, and so do AI clients that add the hosted MCP server, such as Claude, ChatGPT and Cursor. The app then sends an access token (moa_…) in the same Authorization: Bearer header. A token lasts one hour and the app renews it by itself, so an app used at least once every 60 days stays connected. A sign-in acts in live mode.

You approve one of three levels of access. An app asks for one, and an app that names none asks for full access:

Sign-in scopeShown asIts tokens hold
readRead onlyread
sendSend & readsend, read
full_accessFull accesssend, read, manage

Nearly every app asks for full_access, because an MCP client asks for every scope the server lists, so on the approval page you can give it Send & read instead.

What you can give an app also depends on your role in the workspace: an owner or admin can give Full access, a member Send & read at most, and a billing member Read only. An app can never do more than the person who connected it, so when that person’s role is lowered, the app is narrowed to match from its next request.

The draft routes also take send, read or manage, so an app never needs draft.

Whatever you approved, an app you connected is refused with 403 connected_app_not_allowed when it tries to:

  • create an API key or a webhook, change where a webhook sends, or rotate a webhook’s signing secret;
  • list or revoke connected apps;
  • delete a thread for good (it can move one to trash);
  • change an agent’s allowlist, blocklist or send limits;
  • open the billing portal.

So an app keeps no way in after you disconnect it. It reads the request log without the IP addresses and browsers the log records. With full access it can still create agents, and a new agent starts without limits. To keep an app inside the limits you set, give it Send & read, which cannot create or change agents, not even by sending from a name no agent has. Or connect it with an API key tied to one agent, which can neither create agents nor change that agent’s limits. The mails CLI is mails.ai’s own app and has none of these limits.

Settings › Connected apps in the dashboard lists the apps connected to the workspace, with who connected each one. Owners and admins can revoke any of them, and other members the ones they connected. Revoking ends the sign-in at once: every token the app holds stops working, and it has to be approved again to get back in. Leaving a workspace disconnects the apps you connected there.

Over the API, GET /v1/oauth/grants lists them and DELETE /v1/oauth/grants/{id} revokes one. Both take a key with manage that is not bound to an agent, or the mails CLI (mails connected-apps list and mails connected-apps revoke). API keys and webhooks an app created keep working after a revoke, and so do webhook signing secrets it was shown: the response counts them, so revoke and rotate those yourself.

Handling keys safely

  • Never commit a key. Load it from an environment variable (MAILS_API_KEY is the convention across the SDKs and MCP server) or a secrets manager. Treat it like a password.
  • Scope minimally. A service that only ingests webhooks needs read, not manage. A send-only worker needs send.
  • Bind to an agent where it helps — a per-agent key means a leak revokes one identity, not your whole workspace.
  • Use test mode in CI. mk_test_ keys never send real mail, so your test suite can exercise the full request and response shape with zero blast radius.

Common questions

What's the difference between live and test mode?

A key is either mk_live_ or mk_test_. Test-mode keys exercise the full API shape without sending real email or incurring cost — use them in CI and local development. Live-mode keys send real mail and meter real usage. The mode is fixed at key creation and visible in the key prefix and in GET /v1/me.

How do I rotate a key?

Mint the replacement first (POST /v1/api-keys or the dashboard), deploy it, then revoke the old one with DELETE /v1/api-keys/{id}. Revocation is immediate — the next request with the revoked key gets 401 revoked_api_key. Because a key is only shown once at creation, there is no way to recover a lost key; rotate instead.

Should I use one key for everything?

No. Scope each key to the least it needs (a webhook-ingesting service only needs read; a send-only worker only needs send) and, where it helps, bind a key to a single agent via agent_id so a leak is contained to one identity. Keys are cheap to mint and free to revoke.

Was this page helpful?