API keys
Read, create, change, rotate and revoke keys, and read what they did.
Runs any of 12 calls on your workspace.
Reading keys
GET /keys lists every key the caller can see, newest first and a page at a time, with its status, scopes, role, send scope, when it was last used and who made and last changed it. GET /keys/{id} reads one. No read ever returns a secret: maskedKey is enough to tell two keys apart. Both need keys:read.
{ "object": "api_key", "id": "4c1b257a66287fd113bd89d0", "name": "Billing sender", "maskedKey": "oe_live_4c1b…kX7a", "status": "active", "scopes": ["emails:send"], "roleId": null, "domainAllowlist": ["billing.acme.com"], "expiresAt": "2026-12-22T09:00:00.000Z", "lastUsedAt": "2026-09-23T08:14:02.000Z", "createdBy": { "kind": "apiKey", "name": "API key Provisioner", "label": "API key Provisioner" }}Creating and changing keys
POST /keysmints a key and returns its secret intoken, once. Left out, the scopes areemails:sendand the role, send scope and expiry are the caller’s own.PATCH /keys/{id}renames a key, replaces its scopes or its send scope, and switches it off and on withenabled. Switching off is the reversible choice: the key keeps everything and is refused withinactive_api_keyuntil it is switched on again.POST /keys/{id}/rotategives a key a new secret and returns it once. The old secret stops working the instant the call returns.POST /keys/{id}/revokeretires a key for good, with an optionalreason.DELETE /keys/{id}then removes it from the list and keeps its history.- Every one of them needs
keys:manage. Rotating the calling key also works withkeys:write, exactly likePOST /keys/self/rotate.
Never wider than the caller
Every change is checked against the key making it. A key that would end up outside the caller on any axis is refused with 403 beyond_caller_authority, and param names the axis:
- Scopes: only scopes the caller holds after its own role has narrowed them.
- Role: a caller capped by a role can only create and manage keys capped by the same role.
- Expiry: a caller that expires can only create and manage keys that expire no later.
- Mode: a test key can only reach test keys.
- Send scope: only domains and addresses inside the caller’s own, and holding one address never covers its whole domain.
A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other key is a 404. Over OAuth only the owner of the workspace reaches these calls, and a member’s token is refused with owner_only.
Before you grant keys:manage
The console asks you to verify yourself again before it mints or rotates a key. A call made with a key cannot be asked that, so keys:manage is a credential that makes credentials: a leaked key holding it can mint keys of its own, up to its own reach, that keep working after it is revoked.
- Give
keys:manageonly to automation whose job is provisioning keys, never to a key that sends mail. - Narrow that key: a role, a send scope and an expiry. Everything it creates inherits all three and can never exceed them.
- Watch
GET /keys/activity. Every key it creates, changes or revokes is recorded against it by name, so a leak shows up as keys you did not expect. keys:readexposes the request log, IP addresses and user agents included. Treat it as audit access.
Request log and activity
GET /keys/requests and GET /keys/{id}/requests read every authenticated call a key made, newest first: method, path, status, error code, duration, IP and user agent, never a body or a query string. keyIds, failedOnly, status, path, since and until narrow it, and they combine. GET /keys/activity and GET /keys/{id}/activity read what happened to keys, with actor naming who did it, as @username or API key <name>. Nothing is pruned, and a deleted key keeps its history.
status takes one or more exact status codes, comma-separated, such as status=401,403. path takes a route such as /emails/:id and matches it whatever the method. It matches the route, not the exact path: an id in the path counts as :id and an email address as :address, so a real id reads its whole route. End it with * to read every route that starts with it, such as /emails/*.
A key reads its own log with no scope at all. GET /keys/self/requests, GET /keys/self/activity and GET /keys/self/stats answer in the same shapes for the key making the call and no other, so a key that can only send can still watch itself. sends in the stats is null unless the key holds emails:read, and an OAuth access token is refused with api_key_only.
Stats
GET /keys/stats reads what the keys did inside a window, the Analytics tab of the API keys page: the mail they sent and what became of it, the calls refused for a bad, revoked or expired secret, the requests they made and how many failed, the routes they called most with the median time each took, and the status codes they got back. It needs keys:read and emails:read. keyIds narrows it to some keys, since and until set the window, 30 days by default, and grain and offsetMinutes shape the series.
{ "object": "api_key_stats", "since": "2026-09-02T00:00:00.000Z", "until": "2026-10-02T00:00:00.000Z", "grain": "day", "keyIds": ["4c1b257a66287fd113bd89d0"], "sends": { "totals": { "sends": 412, "delivered": 405, "bounced": 3, "complained": 0, "failed": 4 } }, "rejected": [{ "bucket": "2026-09-28", "count": 2 }], "requests": [{ "bucket": "2026-09-28", "count": 96, "failed": 1 }], "routes": [{ "label": "POST /emails", "count": 412, "failed": 4, "medianMs": 180 }], "codes": [{ "label": "200", "count": 408 }, { "label": "422", "count": 4 }]}A key narrowed to some domains or addresses counts only the keys whose send scope sits inside its own. A bucket with nothing in it has no entry, so a chart has to fill the gaps.
Reference
/keysFull reference/keysFull reference/keys/{id}Full reference/keys/{id}Full reference/keys/{id}Full reference/keys/{id}/rotateFull reference/keys/{id}/revokeFull reference/keys/requestsFull reference/keys/{id}/requestsFull reference/keys/activityFull reference/keys/statsFull reference/keys/{id}/activityFull reference