---
title: "API keys"
description: "Read, create, change, rotate and revoke keys, and read what they did."
url: "https://openemail.uk/docs/api/keys"
area: "API"
category: "Keys & webhook logs"
---

# API keys

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

**Also documents:** `GET /keys`, `POST /keys`, `GET /keys/{id}`, `PATCH /keys/{id}`, `DELETE /keys/{id}`, `POST /keys/{id}/rotate`, `POST /keys/{id}/revoke`, `GET /keys/requests`, `GET /keys/{id}/requests`, `GET /keys/activity`, `GET /keys/{id}/activity`

## 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.
