API keys
How MachineMail API keys work, how to scope them to a pod or a single inbox, and how to limit them to specific permissions.
Every request to /v0 and /mcp authenticates with a Bearer token:
Keys start with mm_live_ followed by 32 base62 characters. MachineMail stores only a SHA-256 hash of each key, so the full secret is shown once, when the key is created. The first 12 characters are kept as prefix so you can recognise a key in lists.
Your first key is created when you first sign in and shown once in the dashboard. Create more at machinemail.dev/app/api-keys or through the API.
A request without a valid key gets 401 unauthorized with a WWW-Authenticate: Bearer resource_metadata="..." header. Agents use that header to discover auth.md self-registration.
Scope
A key has one of three scopes. The scope is fixed when the key is minted and decided by the route you mint it on.
| Scope | Minted at | Can reach |
|---|---|---|
| Organization | POST /v0/api-keys | Everything in the organization. |
| Pod | POST /v0/pods/{pod_id}/api-keys | Resources in one pod. |
| Inbox | POST /v0/inboxes/{inbox_id}/api-keys | One inbox: its messages, threads, drafts, and webhooks and lists scoped to it. |
Out-of-scope resources return 404, as if they did not exist. Some operations need a wider scope and return 403:
- Only organization keys can mint, and so only they can create pod and inbox keys. Pod and inbox keys cannot create any keys.
- Only organization keys can create pods and remove suppressions.
- Inbox keys cannot create or delete inboxes, or add domains.
Use an inbox key when you hand credentials to a single agent: a leaked key exposes one mailbox, not the account.
Creating a key
| Body field | Notes |
|---|---|
name | Label for the key. Defaults to “API key”. |
permissions | Optional allowlist. An object of booleans ({ "message_read": true }) or an array of names. Omit it for full access within the scope. |
expires_at | Optional ISO 8601 time. The key stops working at that moment. |
Permissions
Without permissions, a key can do everything its scope allows. With permissions, every request is checked against the list and refused with 403 forbidden (“API key lacks the … permission”) if the needed permission is missing. The names are the same as AgentMail’s.
| Area | Permissions |
|---|---|
| Inboxes | inbox_read, inbox_create, inbox_update, inbox_delete |
| Messages | message_read, message_send, message_reply, message_update, message_delete |
| Threads | thread_read, thread_delete (label changes on threads need message_update) |
| Hidden labels | label_spam_read, label_blocked_read, label_unauthenticated_read, label_trash_read |
| Drafts | draft_read, draft_create, draft_update, draft_delete, draft_send |
| Webhooks | webhook_read, webhook_create, webhook_update, webhook_delete |
| Domains | domain_read, domain_create, domain_update, domain_delete |
| Lists and suppressions | list_entry_read, list_entry_create, list_entry_delete |
| Metrics and events | metrics_read |
| API keys | api_key_read, api_key_create, api_key_update, api_key_delete |
| Pods | pod_read, pod_create, pod_update, pod_delete |
| Raw injection | inbound_raw (MachineMail only: POST /inbound/raw). Never implied by full access |
Some details worth knowing:
- Replies accept either
message_replyormessage_send. Forwarding and new messages needmessage_send. - Attachments, raw MIME,
batch-getand inbox export needmessage_read. - Webhook test and delivery replay need
webhook_update. Domain verify needsdomain_update. - The event log and the SSE stream need
metrics_read. The WebSocket needs any ofmessage_read,inbox_read,domain_read,draft_readorwebhook_read. Both streams send each event only when the key holds its read permission (message.*needsmessage_read, and so on; see Events). - Without the matching
label_*_readpermission, a key never sees messages labelledSPAM,BLOCKED,UNAUTHENTICATEDorTRASH, even if it asks for them withinclude_spamand friends. - A key with limited permissions can only mint keys with a subset of its own permissions, and cannot mint an unrestricted key.
inbound_rawis opt-in: full-access keys do not have it, and only the dashboard or a key that already has it can grant it.{"*": true, "inbound_raw": true}means full access plus raw injection.
Managing keys
| Method | Path | Permission |
|---|---|---|
GET | /v0/api-keys | api_key_read |
POST | /v0/api-keys | api_key_create |
GET | /v0/api-keys/{api_key_id} | api_key_read |
PATCH | /v0/api-keys/{api_key_id} | api_key_update |
DELETE | /v0/api-keys/{api_key_id} | api_key_delete |
The same routes exist under /v0/pods/{pod_id}/api-keys and /v0/inboxes/{inbox_id}/api-keys, where listing shows only the keys of that pod or inbox. Revoked keys are left out of lists.
PATCH changes name and permissions. Scope and expiry cannot be changed; mint a new key instead.
DELETE revokes the key. It stops working immediately. The record is kept with revoked_at set.
A listed key looks like this. The secret is never returned again.
used_at is updated at most once a minute, not on every request.
Agent access tokens
Agents that register through auth.md do not get an mm_live_ key. They exchange an assertion for a short-lived JWT access token and send it the same way, as Authorization: Bearer <token>. Before the agent is claimed, the token reaches only its sandbox inbox. See Claiming agents.