Authentication and API keys

Send your key in the Authorization header on every /v0 request.

curl "$BASE_URL/v0/inboxes" -H "Authorization: Bearer $API_KEY"

Keys start with am_. Only a hash is stored, so the secret cannot be shown again. A missing or unknown key returns 401. Browsers cannot set headers on a WebSocket, so /v0 routes also accept ?api_key=; URLs are logged by proxies, so use it only for browser WebSockets. The hosted MCP endpoint (/mcp) accepts the header only.

The dashboard talks to the same API with a session cookie. Programmatic access uses keys.

Create a key#

curl -X POST "$BASE_URL/v0/api-keys" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "reader", "inbox_id": "'$INBOX_ID'", "permissions": ["message_read", "inbox_read"]}'

permissions is a list of names, or an object such as {"message_read": true}. Omit it for full access within the scope. A key cannot be given a wider scope than the key that creates it, and when the creating key has a permission list, any permission outside that list is dropped from the new key without an error. Unknown names are rejected with 400 validation_error.

Scope#

Key scopeCan reach
Organization (default)Everything in the organization.
Pod (pod_id)Only that pod's inboxes, webhooks, domains and lists.
Inbox (inbox_id)Only that inbox. Cannot create inboxes, pods or domains.

Resources outside a key's scope answer 404, not 403, so their existence is not revealed. A scoped key that tries something its scope never allows, such as creating a pod or an inbox, gets 403 missing_permission.

Permissions#

account_update, api_key_create, api_key_delete, api_key_read, api_key_update, app_connect, app_share_owner, domain_create, domain_delete, domain_read, domain_update, draft_create, draft_delete, draft_read, draft_send, draft_update, inbox_create, inbox_delete, inbox_read, inbox_update, label_blocked_read, label_spam_read, label_trash_read, label_unauthenticated_read, list_entry_create, list_entry_delete, list_entry_read, message_delete, message_read, message_reply, message_send, message_update, metrics_read, pod_create, pod_delete, pod_read, pod_update, thread_delete, thread_read, webhook_create, webhook_delete, webhook_read, webhook_update.

The names provider_connect and provider_share_owner were renamed to app_connect and app_share_owner and are rejected. Billing routes need an organization-level key without a permission list: billing_read and billing_manage cannot be granted in a list.

Email verification and agent sign-up#

A new account can receive mail immediately, but sending and creating API keys require a verified owner email. Without verification the API answers 403 with code email_unverified.

An agent can also create its own account with POST /v0/agent/sign-up (no authentication, rate limited per IP), which returns an api_key, an inbox_id and an organization_id:

curl -X POST "$BASE_URL/v0/agent/sign-up" -H "Content-Type: application/json" \
  -d '{"username": "my-agent", "human_email": "owner@example.com"}'

Sending from such an account is locked (403 send_locked) until a human is attached with POST /v0/agent/attach-human and the 6-digit code emailed to them is submitted to POST /v0/agent/verify as otp_code. Codes expire after 24 hours and allow 10 attempts (429 too_many_attempts after that). The human can be replaced at most twice (409 human_replacement_limit). Sign-ups are rate limited per IP.

Dashboard sign-in#

People sign in to the dashboard with email and password (and a two-step code if enabled). That uses a session cookie, not an API key, and has its own endpoints under /auth. See security for what protects it.