Skip to the documentation
API

API keys

Read, create, change, rotate and revoke keys, and read what they did.

GETapi.openemail.uk/keys

Runs any of the 11 calls on this page against your workspace, with your own key.

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.

GET /keys/4c1b257a66287fd113bd89d0
{  "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 /keys mints a key and returns its secret in token, once. Left out, the scopes are emails:send and 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 with enabled. Switching off is the reversible choice: the key keeps everything and is refused with inactive_api_key until it is switched on again.
  • POST /keys/{id}/rotate gives a key a new secret and returns it once. The old secret stops working the instant the call returns.
  • POST /keys/{id}/revoke retires a key for good, with an optional reason. 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 with keys:write, exactly like POST /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:manage only 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:read exposes 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, since and until are the filters the console offers. 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.