Authentication & key scoping
Every /api/v1/ request authenticates with a bearer API key. Keys are opaque secrets that
start with pf_ and are stored only as a SHA-256 hash server-side.
Authorization: Bearer pf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
A missing or malformed header returns 401 {"error": "missing bearer token"}; an unknown key
returns 401 {"error": "invalid api key"}.
Key scopes
Each key carries one scope. Only three scopes can authenticate a bearer request:
bootstrap— provision-only. Accepted byPOST /api/v1/mailboxes(to create a mailbox) and nothing else.api— the standard key for direct HTTP use.mcp— the standard key for the MCP server.
api and mcp are interchangeable across the data-plane endpoints (send, read, drafts). A
key whose scope is not permitted for an endpoint returns
403 {"error": "scope '<scope>' not permitted for this endpoint"}.
Mailbox-bound keys
A key is either account-wide (mailbox_id is null) or bound to a single mailbox
(mailbox_id set). A bound key can only ever act on its one mailbox.
Each key also carries two capability flags:
can_read— may read messages and drafts (GETroutes).can_send— may send and create/edit/delete/send drafts (POST/PATCH/DELETE).
Both default to true; a key is restricted only when a flag is explicitly set false.
403 key_scope
When a key lacks the capability or targets a different mailbox, the response is 403 with
code: "key_scope":
{ "error": "this api key cannot send", "code": "key_scope" }
You will see code: "key_scope" in these cases:
| Situation | Example message |
|---|---|
can_send=false on a send/draft-write endpoint |
this api key cannot send |
can_read=false on a read endpoint |
this api key cannot read messages |
Bound key, body mailbox_id is a different mailbox |
this api key is scoped to a different mailbox |
| Bound key calls an account-plane endpoint (mailboxes/domains) | this api key is scoped to a single mailbox |
Data plane vs. control plane
Postfleet splits its surface deliberately. API keys operate the data plane. Sensitive control-plane operations are dashboard-only and are never exposed to a bearer key.
Data plane (API keys):
- Send email —
POST /api/v1/send - Read messages —
GET /api/v1/messages,GET /api/v1/messages/:id - Drafts —
POST/GET /api/v1/drafts,GET/PATCH/DELETE /api/v1/drafts/:id,POST /api/v1/drafts/:id/send - Provision mailboxes and custom domains —
POST /api/v1/mailboxes,/api/v1/domains(account-wide keys only — a mailbox-bound key gets403 key_scopehere)
Control plane (dashboard only — no API):
- Creating and revoking API keys
- Managing send allow/blocklists
- Approving or rejecting pending drafts
- Toggling a mailbox's
require_approvalsetting - Configuring the webhook URL/secret and redriving dead-lettered webhook deliveries
An agent holding a data-plane key therefore cannot escalate its own permissions, edit policy, approve its own drafts, or replay webhooks — those levers stay with a human.
Anti-enumeration: cross-scope ids return 404
Resource-id endpoints never confirm that an id exists outside your scope. A message or draft
id that belongs to another account, or (for a mailbox-bound key) to another mailbox in
your own account, returns the same 404 as a truly missing id:
{ "error": "message not found" }
{ "error": "draft not found" }
This is byte-identical to the "never existed" response, so a probe cannot tell "not yours"
from "does not exist". (Body-supplied mailbox_id mismatches on POST /api/v1/send and
POST /api/v1/drafts are the exception — those are 403 key_scope, since a body field is a
scope violation rather than an id probe.)