Agent Mail by Ackuna

Every page here is also markdown — add .md to the URL.

Authentication

Every /v1 request authenticates with a bearer API key: Authorization: Bearer ak_.... There is no session, cookie, or OAuth flow.

GET /v1/me
Authorization: Bearer ak_9f2c1e7b4a6d8035f1c9a2b7e4d6f803a1b2c3d4
{
  "data": {
    "object": "account",
    "organization": {"id": "org_01k2n9v8qf3m7d5b0y6xz4rt1c", "name": "Acme Robotics"},
    "api_key": {"id": "key_01k2n9v8qf3m7d5b0y6xz4rt2d", "name": "prod agent", "scopes": ["inboxes:read", "messages:read", "messages:send"]},
    "access": "active",
    "quotas": {
      "inboxes": {"limit": 3, "used": 0},
      "outbound_daily": {"limit": 100, "used": 0, "resets_at": "2026-08-21T23:59:59.999999Z"},
      "outbound_daily_shared_domain": {"limit": 100, "used": 0, "workspace_used": 0, "resets_at": "2026-08-21T23:59:59.999999Z"},
      "outbound_monthly": {"limit": 3000, "used": 0, "scope": "service", "service_used": 0, "resets_at": "2026-08-31T23:59:59.999999Z"},
      "webhook_endpoints": {"limit": 1, "used": 0},
      "custom_domains": {"limit": 0, "used": 0}
    }
  }
}

GET /v1/me requires no scope — a key must always be able to discover what it can do, even one with no scopes at all. Call it first in any new integration.

Keys

A key is issued once — by sign-up, in the console under Developers → API keys, or by an operator — and shown in plaintext exactly that one time: ak_ followed by 40 random characters. It is stored server-side only as a hash — there is no way to retrieve the plaintext again; issue a new key if it is lost. A key can carry an expires_at; once past, every request with it fails with 401 unauthenticated, the same as a revoked or malformed key.

Access states

Every /v1 request is checked against the organization's agent_mail_access before scopes: active passes; none answers 403 agent_mail_not_enabled; suspended answers 403 organization_suspended. access on GET /v1/me reports the current value.

Scopes

Each key carries a list of scopes, checked per-endpoint after the access check passes:

Scope Grants
inboxes:read List and fetch inboxes
inboxes:write Create and update inboxes
messages:read Read threads, messages and attachments
messages:send Send a message from an inbox, and retry a failed one
webhooks:read List webhook endpoints and their deliveries
webhooks:write Create, update, delete, rotate, and redrive webhooks
domains:read List and fetch receiving domains
domains:write Register and delete receiving domains

A key missing a required scope gets 403 insufficient_scope, and message names exactly which scope was missing so the failure is actionable without guessing. GET /v1/me reports the key's current scopes under api_key.scopes.

Rate limits

Two independent limiters apply to every /v1 request except sign-up, checked before scopes:

Either limit failing returns 429 rate_limited with a Retry-After header giving seconds until the window resets.

Sending has a third limit of its own. POST /v1/inboxes/{inbox}/messages and POST /v1/messages/{message}/retry also count against an hourly send limit for the key's workspace. Once the workspace has been reviewed, the limit comes from its plan: 13 send requests an hour on Free, 250 on Builder, 1,250 on Scale and 6,250 on Startup. Until then it is 100 an hour, though a workspace not yet reviewed is held to a much smaller daily quota anyway. Over it, the send fails with 429 rate_limited and a Retry-After header giving seconds until the window resets.

POST /v1/signup and POST /v1/signup/verify take no key and are not covered by any of these limits; they carry their own, stricter ones — documented in the OpenAPI description.

Quotas

Beyond rate limits, six quotas apply, and all are visible on GET /v1/me under quotas. Two of them are counted across the whole customer account rather than this workspace alone: outbound_daily_shared_domain, and outbound_monthly when the plan's allowance is pooled (scope is pooled; service_used is then Agent Mail's own share of used):

Structural caps do not clear with time — only a larger grant helps. The daily and monthly caps always clear at their next reset times.

Idempotency

Every mutating endpoint (POST, PATCH, DELETE) accepts an optional Idempotency-Key header — any client-generated string up to 255 characters:

POST /v1/inboxes
Idempotency-Key: create-support-inbox-1

The header is optional — omitting it processes every request normally — but strongly recommended on anything that sends mail or creates a resource, since retries after a network timeout are otherwise indistinguishable from a second, deliberate request. Stored keys expire after 24 hours.