---
title: "API keys"
description: "Every operation in this group: what it accepts, what it returns and the errors it can answer with."
url: "https://openemail.uk/docs/api/reference/api-keys"
area: "API"
category: "Reference"
---

# API keys

Every operation in this group: what it accepts, what it returns and the errors it can answer with.

## Operations

The keys of the workspace, their request log and their activity: the Settings, API keys page of the app. Reading needs `keys:read`; creating, changing, rotating, switching, revoking and deleting need `keys:manage`, a scope no key holds unless somebody gave it one. Step-up verification, which the app asks for before it mints or rotates a key, cannot apply to a call made with a key, so treat `keys:manage` as a credential that can make credentials: give it only to automation that provisions keys, narrow that key to the send scope and role it needs, and give it an expiry. A key never makes or reaches a key wider than itself.

### `GET /keys`

List API keys

Every key in the workspace, newest first, a page at a time, with its status, scopes, role, send scope, when it was last used and who made and last changed it. Revoked and expired keys stay listed until somebody deletes them. No secret is ever returned here.

A key never makes or reaches a key wider than itself. What it creates, changes, rotates, switches, revokes or deletes has to sit inside it on every axis: scopes the caller holds (after its own role has narrowed them), the same role when the caller has one, an expiry no later than the caller's when the caller expires, the caller's mode, and a send scope inside the caller's own, where holding one address never covers its whole domain. A key narrowed to some domains or addresses also only SEES the keys whose send scope sits inside its own; any other is a 404. Over OAuth only the workspace owner reaches these calls, and a member's token is refused with `owner_only`.

Requires the `keys:read` scope.

- Scopes: `keys:read`.

**Query parameters**

- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `ApiKeyList`: A page of keys.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.list()`](https://openemail.uk/docs/sdk/reference/keys#list), [`keys.listAll()`](https://openemail.uk/docs/sdk/reference/keys#listAll), [`keys.iterate()`](https://openemail.uk/docs/sdk/reference/keys#iterate); CLI [`openemail keys list`](https://openemail.uk/docs/cli/reference/keys#keys-list); MCP [`listApiKeys`](https://openemail.uk/docs/mcp/tools/keys#listApiKeys).

### `POST /keys`

Create an API key

Mints a live key and returns its secret in `token`, once. Nothing recovers it afterwards, so store it before doing anything else.

Left out, `scopes` is `emails:send`, the role is the caller's own (or none), the send scope is the caller's own (or none, which means every address the workspace owns), and the expiry is the caller's (or none). The workspace cap on live keys applies, as a 422 `workspace_limit_reached`. The new key is attributed to the key that made it in the activity log, and it can make keys of its own only if it was given `keys:manage`.

A key never makes or reaches a key wider than itself. What it creates, changes, rotates, switches, revokes or deletes has to sit inside it on every axis: scopes the caller holds (after its own role has narrowed them), the same role when the caller has one, an expiry no later than the caller's when the caller expires, the caller's mode, and a send scope inside the caller's own, where holding one address never covers its whole domain. A key narrowed to some domains or addresses also only SEES the keys whose send scope sits inside its own; any other is a 404. Over OAuth only the workspace owner reaches these calls, and a member's token is refused with `owner_only`.

Requires the `keys:manage` scope.

- Scopes: `keys:manage`.

**Request body**

- `name` (`string`, required, 1 to 60 characters): A name, 1 to 60 characters.
- `scopes` (`string[]`, at least one item, one of `"emails:send"`, `"emails:read"`, `"drafts:read"`, `"drafts:write"`, `"threads:read"`, `"threads:write"`, `"files:read"`, `"files:write"`, `"labels:read"`, `"labels:write"`, `"contacts:read"`, `"contacts:write"`, `"audiences:read"`, `"audiences:write"`, `"calendar:read"`, `"calendar:write"`, `"templates:read"`, `"templates:write"`, `"domains:read"`, `"domains:write"`, `"webhooks:read"`, `"webhooks:write"`, `"rules:read"`, `"rules:write"`, `"connections:read"`, `"members:read"`, `"members:write"`, `"roles:read"`, `"roles:write"`, `"settings:read"`, `"settings:write"`, `"keys:write"`, `"keys:read"`, `"keys:manage"`, `"forms:read"`, `"forms:write"`, `"billing:read"`, `"billing:write"`, `"account:read"`, `"account:write"`, default `["emails:send"]`): The scopes the key holds. Left out, `emails:send`. Each one has to be held by the caller.
- `roleId` (`string`, nullable, 1 to 64 characters): A role to cap the key. Left out, the caller's own role, or none. A caller with a role can only give its own.
- `addressAllowlist` (`string[]`, up to 50 items): Single addresses the key may send as. Left out together with `domainAllowlist`, the caller's own send scope.
- `domainAllowlist` (`string[]`, up to 25 items): Whole domains the key may send as, including addresses added to them later.
- `expiresInMinutes` (`integer`, at least 5, at most 5256000): Minutes until the key expires, 5 to 5,256,000 (ten years). Left out, the caller's own expiry, or none.

**Returns**

- `201` `object`: The key, with its secret.
  - `object` (`string`, one of `"api_key"`)
  - `id` (`string`): 24 hex characters, the part of the token after `oe_live_`.
  - `name` (`string`)
  - `mode` (`string`, one of `"live"`, `"test"`)
  - `maskedKey` (`string`): Enough of the token to tell two keys apart, such as `oe_live_4c1b…kX7a`.
  - `keyLast4` (`string`)
  - `status` (`string`, one of `"revoked"`, `"expired"`, `"inactive"`, `"active"`)
  - `scopes` (`string[]`, one of `"emails:send"`, `"emails:read"`, `"drafts:read"`, `"drafts:write"`, `"threads:read"`, `"threads:write"`, `"files:read"`, `"files:write"`, `"labels:read"`, `"labels:write"`, `"contacts:read"`, `"contacts:write"`, `"audiences:read"`, `"audiences:write"`, `"calendar:read"`, `"calendar:write"`, `"templates:read"`, `"templates:write"`, `"domains:read"`, `"domains:write"`, `"webhooks:read"`, `"webhooks:write"`, `"rules:read"`, `"rules:write"`, `"connections:read"`, `"members:read"`, `"members:write"`, `"roles:read"`, `"roles:write"`, `"settings:read"`, `"settings:write"`, `"keys:write"`, `"keys:read"`, `"keys:manage"`, `"forms:read"`, `"forms:write"`, `"billing:read"`, `"billing:write"`, `"account:read"`, `"account:write"`)
  - `roleId` (`string`, nullable): The role that caps the key, if any.
  - `roleName` (`string`, nullable)
  - `addressAllowlist` (`string[]`, nullable)
  - `domainAllowlist` (`string[]`, nullable)
  - `expiresAt` (`string`, nullable, format `date-time`)
  - `lastUsedAt` (`string`, nullable, format `date-time`)
  - `totalUses` (`integer`): Calls the key has made, leaving out the ones refused before it authenticated.
  - `rotatedAt` (`string`, nullable, format `date-time`)
  - `rotationCount` (`integer`)
  - `deactivatedAt` (`string`, nullable, format `date-time`)
  - `revokedAt` (`string`, nullable, format `date-time`)
  - `revokedReason` (`string`, nullable)
  - `createdAt` (`string`, format `date-time`)
  - `createdBy` (`AuditActor`)
  - `updatedAt` (`string`, format `date-time`)
  - `updatedBy` (`AuditActor`)
  - `lastChangeAt` (`string`, nullable, format `date-time`)
  - `lastChangeType` (`string`, nullable)
  - `token` (`string`): The whole secret, `oe_live_…`, shown this once.

**Errors**

- `403`: `insufficient_scope`: the caller lacks `keys:manage`. `beyond_caller_authority`: the key asked for would be wider than the caller, and `param` names the axis (`scopes`, `roleId`, `mode`, `expiresInMinutes` or `addressAllowlist`). `owner_only`: a member's OAuth token.
- `422`: `invalid_parameter` or `unknown_parameter` on a field, `role_not_found` on `roleId`, a domain or address the workspace does not own, or `workspace_limit_reached`.
- The errors every operation can return: `400`, `401`, `404`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.create()`](https://openemail.uk/docs/sdk/reference/keys#create); CLI [`openemail keys create`](https://openemail.uk/docs/cli/reference/keys#keys-create).

### `GET /keys/requests`

List the request log

Every authenticated call the workspace's keys made, newest first, the Requests tab of the app: method, path, status, error code, duration, IP and user agent, never a body or a query string. Calls refused before a key could be identified are not in it, and neither are calls made with an OAuth access token. Nothing is pruned. `keyIds`, `failedOnly`, `since` and `until` are the filters the app offers.

A narrowed key reads only the log of the keys it can see.

Requires the `keys:read` scope.

- Scopes: `keys:read`.

**Query parameters**

- `keyIds` (`string`): Comma-separated key ids, at most 50. Left out, every key in the workspace.
- `failedOnly` (`boolean`, default `false`): Only calls answered with a status of 400 or more.
- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `ApiKeyRequestList`: A page of calls, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.listWorkspaceRequests()`](https://openemail.uk/docs/sdk/reference/keys#listWorkspaceRequests), [`keys.listAllWorkspaceRequests()`](https://openemail.uk/docs/sdk/reference/keys#listAllWorkspaceRequests), [`keys.iterateWorkspaceRequests()`](https://openemail.uk/docs/sdk/reference/keys#iterateWorkspaceRequests); CLI [`openemail keys list-workspace-requests`](https://openemail.uk/docs/cli/reference/keys#keys-list-workspace-requests); MCP [`listApiKeyRequests`](https://openemail.uk/docs/mcp/tools/keys#listApiKeyRequests).

### `GET /keys/activity`

List API key activity

What happened to the workspace's keys, newest first, the Activity tab of the app: created, updated, rotated, deactivated, reactivated, revoked, deleted, and every attempt to authenticate with a key that was refused. `actor` says who, a person as `@username` or a key as `API key <name>`, and `detail.source` where from. A deleted key keeps its history. `keyIds`, `since` and `until` narrow it.

A narrowed key reads only the activity of the keys it can see.

Requires the `keys:read` scope.

- Scopes: `keys:read`.

**Query parameters**

- `keyIds` (`string`): Comma-separated key ids, at most 50. Left out, every key in the workspace.
- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `ApiKeyActivityList`: A page of changes, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.listWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/keys#listWorkspaceActivity), [`keys.listAllWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/keys#listAllWorkspaceActivity), [`keys.iterateWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/keys#iterateWorkspaceActivity); CLI [`openemail keys list-workspace-activity`](https://openemail.uk/docs/cli/reference/keys#keys-list-workspace-activity); MCP [`listApiKeyActivity`](https://openemail.uk/docs/mcp/tools/keys#listApiKeyActivity).

### `GET /keys/{id}`

Retrieve an API key

One key, without its secret. A key the caller cannot see is a 404.

Requires the `keys:read` scope.

- Scopes: `keys:read`.

**Path parameters**

- `id` (`string`, required): The key id, 24 hex characters.

**Returns**

- `200` `ApiKey`: The key.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.get()`](https://openemail.uk/docs/sdk/reference/keys#get); CLI [`openemail keys get`](https://openemail.uk/docs/cli/reference/keys#keys-get); MCP [`listApiKeys`](https://openemail.uk/docs/mcp/tools/keys#listApiKeys).

### `PATCH /keys/{id}`

Update an API key

Renames a key, replaces its scopes or its send scope, or switches it off and on with `enabled`. A switched-off key is refused on every call with `inactive_api_key` and keeps everything it had, so switching it back on restores it exactly; that is the reversible alternative to revoking. `scopes`, `addressAllowlist` and `domainAllowlist` REPLACE what the key had, and a list left out stays as it was. A revoked key cannot be changed.

A key never makes or reaches a key wider than itself. What it creates, changes, rotates, switches, revokes or deletes has to sit inside it on every axis: scopes the caller holds (after its own role has narrowed them), the same role when the caller has one, an expiry no later than the caller's when the caller expires, the caller's mode, and a send scope inside the caller's own, where holding one address never covers its whole domain. A key narrowed to some domains or addresses also only SEES the keys whose send scope sits inside its own; any other is a 404. Over OAuth only the workspace owner reaches these calls, and a member's token is refused with `owner_only`. A key changing itself may only narrow itself.

Requires the `keys:manage` scope.

- Scopes: `keys:manage`.

**Path parameters**

- `id` (`string`, required): The key id, 24 hex characters.

**Request body**

- `name` (`string`, 1 to 60 characters): A new name, 1 to 60 characters.
- `scopes` (`string[]`, at least one item, one of `"emails:send"`, `"emails:read"`, `"drafts:read"`, `"drafts:write"`, `"threads:read"`, `"threads:write"`, `"files:read"`, `"files:write"`, `"labels:read"`, `"labels:write"`, `"contacts:read"`, `"contacts:write"`, `"audiences:read"`, `"audiences:write"`, `"calendar:read"`, `"calendar:write"`, `"templates:read"`, `"templates:write"`, `"domains:read"`, `"domains:write"`, `"webhooks:read"`, `"webhooks:write"`, `"rules:read"`, `"rules:write"`, `"connections:read"`, `"members:read"`, `"members:write"`, `"roles:read"`, `"roles:write"`, `"settings:read"`, `"settings:write"`, `"keys:write"`, `"keys:read"`, `"keys:manage"`, `"forms:read"`, `"forms:write"`, `"billing:read"`, `"billing:write"`, `"account:read"`, `"account:write"`): The whole new list of scopes, at least one.
- `addressAllowlist` (`string[]`, up to 50 items): The whole new list of single addresses.
- `domainAllowlist` (`string[]`, up to 25 items): The whole new list of whole domains.
- `enabled` (`boolean`): False switches the key off, true switches it back on. Reversible, unlike revoking.

**Returns**

- `200` `ApiKey`: The key as it now stands.

**Errors**

- `403`: `insufficient_scope`, `beyond_caller_authority` or `owner_only`.
- `409`: `revoked`: a revoked key cannot be changed.
- The errors every operation can return: `400`, `401`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.update()`](https://openemail.uk/docs/sdk/reference/keys#update); CLI [`openemail keys update`](https://openemail.uk/docs/cli/reference/keys#keys-update); MCP [`setApiKeyEnabled`](https://openemail.uk/docs/mcp/tools/keys#setApiKeyEnabled), [`setApiKeySendScope`](https://openemail.uk/docs/mcp/tools/keys#setApiKeySendScope).

### `DELETE /keys/{id}`

Delete an API key

Removes a revoked key from the list. Its request log and its activity stay, under `Deleted key`. A key that has not been revoked is refused, so nothing still calling with it can lose its credential without somebody deciding to first.

Requires the `keys:manage` scope.

- Scopes: `keys:manage`.

**Path parameters**

- `id` (`string`, required): The key id, 24 hex characters.

**Returns**

- `200`: `{ object: "api_key", id, deleted: true }`.

**Errors**

- `409`: `not_revoked`: revoke the key first.
- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.delete()`](https://openemail.uk/docs/sdk/reference/keys#delete); CLI [`openemail keys delete`](https://openemail.uk/docs/cli/reference/keys#keys-delete).

### `POST /keys/{id}/rotate`

Rotate an API key

Gives a key a new secret and returns it in `token`, once. The id, name, scopes, role, send scope, expiry and request history all carry on, and the old secret stops working the instant this returns, with no overlap window. Rotating the calling key itself follows `POST /keys/self/rotate` and is allowed with `keys:write` as well as `keys:manage`. A revoked or expired key cannot be rotated.

A key never makes or reaches a key wider than itself. What it creates, changes, rotates, switches, revokes or deletes has to sit inside it on every axis: scopes the caller holds (after its own role has narrowed them), the same role when the caller has one, an expiry no later than the caller's when the caller expires, the caller's mode, and a send scope inside the caller's own, where holding one address never covers its whole domain. A key narrowed to some domains or addresses also only SEES the keys whose send scope sits inside its own; any other is a 404. Over OAuth only the workspace owner reaches these calls, and a member's token is refused with `owner_only`. Rotation hands the caller a working secret for the key, which is why a key that is wider than the caller in any way is refused.

Requires the `keys:manage` scope.

- Scopes: `keys:manage`.

**Path parameters**

- `id` (`string`, required): The key id, 24 hex characters.

**Returns**

- `200` `object`: The key, with its new secret.
  - `object` (`string`, one of `"api_key"`)
  - `id` (`string`): 24 hex characters, the part of the token after `oe_live_`.
  - `name` (`string`)
  - `mode` (`string`, one of `"live"`, `"test"`)
  - `maskedKey` (`string`): Enough of the token to tell two keys apart, such as `oe_live_4c1b…kX7a`.
  - `keyLast4` (`string`)
  - `status` (`string`, one of `"revoked"`, `"expired"`, `"inactive"`, `"active"`)
  - `scopes` (`string[]`, one of `"emails:send"`, `"emails:read"`, `"drafts:read"`, `"drafts:write"`, `"threads:read"`, `"threads:write"`, `"files:read"`, `"files:write"`, `"labels:read"`, `"labels:write"`, `"contacts:read"`, `"contacts:write"`, `"audiences:read"`, `"audiences:write"`, `"calendar:read"`, `"calendar:write"`, `"templates:read"`, `"templates:write"`, `"domains:read"`, `"domains:write"`, `"webhooks:read"`, `"webhooks:write"`, `"rules:read"`, `"rules:write"`, `"connections:read"`, `"members:read"`, `"members:write"`, `"roles:read"`, `"roles:write"`, `"settings:read"`, `"settings:write"`, `"keys:write"`, `"keys:read"`, `"keys:manage"`, `"forms:read"`, `"forms:write"`, `"billing:read"`, `"billing:write"`, `"account:read"`, `"account:write"`)
  - `roleId` (`string`, nullable): The role that caps the key, if any.
  - `roleName` (`string`, nullable)
  - `addressAllowlist` (`string[]`, nullable)
  - `domainAllowlist` (`string[]`, nullable)
  - `expiresAt` (`string`, nullable, format `date-time`)
  - `lastUsedAt` (`string`, nullable, format `date-time`)
  - `totalUses` (`integer`): Calls the key has made, leaving out the ones refused before it authenticated.
  - `rotatedAt` (`string`, format `date-time`)
  - `rotationCount` (`integer`)
  - `deactivatedAt` (`string`, nullable, format `date-time`)
  - `revokedAt` (`string`, nullable, format `date-time`)
  - `revokedReason` (`string`, nullable)
  - `createdAt` (`string`, format `date-time`)
  - `createdBy` (`AuditActor`)
  - `updatedAt` (`string`, format `date-time`)
  - `updatedBy` (`AuditActor`)
  - `lastChangeAt` (`string`, nullable, format `date-time`)
  - `lastChangeType` (`string`, nullable)
  - `token` (`string`): The new secret, shown this once.

**Errors**

- `403`: `insufficient_scope`, `beyond_caller_authority` or `owner_only`.
- `409`: `revoked` or `expired`: create a new key instead.
- The errors every operation can return: `400`, `401`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.rotate()`](https://openemail.uk/docs/sdk/reference/keys#rotate); CLI [`openemail keys rotate`](https://openemail.uk/docs/cli/reference/keys#keys-rotate); MCP [`rotateApiKey`](https://openemail.uk/docs/mcp/tools/keys#rotateApiKey).

### `POST /keys/{id}/revoke`

Revoke an API key

Revokes a key for good: every later call with it is refused with `revoked_api_key`, and it can never be switched back on, rotated or changed. The body is optional and may carry a `reason`, kept on the key. Revoking a key that is already revoked changes nothing and answers the same. A key may revoke itself, which is how an integration that suspects its secret has leaked retires it at once.

A key never makes or reaches a key wider than itself. What it creates, changes, rotates, switches, revokes or deletes has to sit inside it on every axis: scopes the caller holds (after its own role has narrowed them), the same role when the caller has one, an expiry no later than the caller's when the caller expires, the caller's mode, and a send scope inside the caller's own, where holding one address never covers its whole domain. A key narrowed to some domains or addresses also only SEES the keys whose send scope sits inside its own; any other is a 404. Over OAuth only the workspace owner reaches these calls, and a member's token is refused with `owner_only`.

Requires the `keys:manage` scope.

- Scopes: `keys:manage`.

**Path parameters**

- `id` (`string`, required): The key id, 24 hex characters.

**Request body**

- `reason` (`string`, up to 200 characters): Why, at most 200 characters.

**Returns**

- `200` `ApiKey`: The key, now revoked.

**Errors**

- `403`: `insufficient_scope`, `beyond_caller_authority` or `owner_only`.
- The errors every operation can return: `400`, `401`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.revoke()`](https://openemail.uk/docs/sdk/reference/keys#revoke); CLI [`openemail keys revoke`](https://openemail.uk/docs/cli/reference/keys#keys-revoke); MCP [`revokeApiKey`](https://openemail.uk/docs/mcp/tools/keys#revokeApiKey).

### `GET /keys/{id}/requests`

List one key's requests

The request log of one key, newest first. A deleted key's log is still readable by a key that is not narrowed.

Requires the `keys:read` scope.

- Scopes: `keys:read`.

**Path parameters**

- `id` (`string`, required): The key id, 24 hex characters.

**Query parameters**

- `failedOnly` (`boolean`, default `false`): Only calls answered with a status of 400 or more.
- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `ApiKeyRequestList`: A page of calls, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.listRequests()`](https://openemail.uk/docs/sdk/reference/keys#listRequests), [`keys.listAllRequests()`](https://openemail.uk/docs/sdk/reference/keys#listAllRequests), [`keys.iterateRequests()`](https://openemail.uk/docs/sdk/reference/keys#iterateRequests); CLI [`openemail keys list-requests`](https://openemail.uk/docs/cli/reference/keys#keys-list-requests); MCP [`listApiKeyRequests`](https://openemail.uk/docs/mcp/tools/keys#listApiKeyRequests).

### `GET /keys/{id}/activity`

List one key's activity

What happened to one key, newest first. A deleted key's history is still readable by a key that is not narrowed.

Requires the `keys:read` scope.

- Scopes: `keys:read`.

**Path parameters**

- `id` (`string`, required): The key id, 24 hex characters.

**Query parameters**

- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `ApiKeyActivityList`: A page of changes, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.listActivity()`](https://openemail.uk/docs/sdk/reference/keys#listActivity), [`keys.listAllActivity()`](https://openemail.uk/docs/sdk/reference/keys#listAllActivity), [`keys.iterateActivity()`](https://openemail.uk/docs/sdk/reference/keys#iterateActivity); CLI [`openemail keys list-activity`](https://openemail.uk/docs/cli/reference/keys#keys-list-activity); MCP [`listApiKeyActivity`](https://openemail.uk/docs/mcp/tools/keys#listApiKeyActivity).

### `GET /keys/stats`

Read API key stats

What the keys of the workspace 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 or revoked secret, the requests they made and how many failed, the 8 routes they called most with the median time each took, and the status codes they got back. Every key you can see, or the ones `keyIds` names. A key limited to some addresses sees only the keys whose send scope sits inside its own, and over OAuth only the owner of the workspace reaches it.

Requires the `keys:read` and `emails:read` scopes.

- Scopes: `keys:read`, `emails:read`.

**Query parameters**

- `keyIds` (`string`): Comma-separated key ids, at most 50. Left out, every key you can see.
- `since` (`string`, format `date-time`): The start of the window, an ISO 8601 instant. Left out, 30 days before `until`.
- `until` (`string`, format `date-time`): The end of the window, an ISO 8601 instant, not included. Left out, now.
- `grain` (`string`, one of `"minute"`, `"hour"`, `"day"`, default `"day"`): How wide one bucket of the series is. The bucket keys change shape with it: `YYYY-MM-DD` for a day, `YYYY-MM-DDTHH` for an hour, `YYYY-MM-DDTHH:MM` for a minute.
- `offsetMinutes` (`integer`, at least -840, at most 840, default `0`): Minutes to add to UTC before cutting the buckets, so a day starts at midnight where the reader is. 120 for UTC+2.

**Returns**

- `200` `ApiKeyStats`: The figures for the window.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`keys.stats()`](https://openemail.uk/docs/sdk/reference/keys#stats); CLI [`openemail keys stats`](https://openemail.uk/docs/cli/reference/keys#keys-stats); MCP [`getApiKeyStats`](https://openemail.uk/docs/mcp/tools/keys#getApiKeyStats).

### Objects

#### `ApiKey`

`object`

An API key without its secret. The secret is returned once, by the call that made or rotated it, and never again.

- `object` (`string`, one of `"api_key"`)
- `id` (`string`): 24 hex characters, the part of the token after `oe_live_`.
- `name` (`string`)
- `mode` (`string`, one of `"live"`, `"test"`)
- `maskedKey` (`string`): Enough of the token to tell two keys apart, such as `oe_live_4c1b…kX7a`.
- `keyLast4` (`string`)
- `status` (`string`, one of `"revoked"`, `"expired"`, `"inactive"`, `"active"`)
- `scopes` (`string[]`, one of `"emails:send"`, `"emails:read"`, `"drafts:read"`, `"drafts:write"`, `"threads:read"`, `"threads:write"`, `"files:read"`, `"files:write"`, `"labels:read"`, `"labels:write"`, `"contacts:read"`, `"contacts:write"`, `"audiences:read"`, `"audiences:write"`, `"calendar:read"`, `"calendar:write"`, `"templates:read"`, `"templates:write"`, `"domains:read"`, `"domains:write"`, `"webhooks:read"`, `"webhooks:write"`, `"rules:read"`, `"rules:write"`, `"connections:read"`, `"members:read"`, `"members:write"`, `"roles:read"`, `"roles:write"`, `"settings:read"`, `"settings:write"`, `"keys:write"`, `"keys:read"`, `"keys:manage"`, `"forms:read"`, `"forms:write"`, `"billing:read"`, `"billing:write"`, `"account:read"`, `"account:write"`)
- `roleId` (`string`, nullable): The role that caps the key, if any.
- `roleName` (`string`, nullable)
- `addressAllowlist` (`string[]`, nullable)
- `domainAllowlist` (`string[]`, nullable)
- `expiresAt` (`string`, nullable, format `date-time`)
- `lastUsedAt` (`string`, nullable, format `date-time`)
- `totalUses` (`integer`): Calls the key has made, leaving out the ones refused before it authenticated.
- `rotatedAt` (`string`, nullable, format `date-time`)
- `rotationCount` (`integer`)
- `deactivatedAt` (`string`, nullable, format `date-time`)
- `revokedAt` (`string`, nullable, format `date-time`)
- `revokedReason` (`string`, nullable)
- `createdAt` (`string`, format `date-time`)
- `createdBy` (`AuditActor`)
- `updatedAt` (`string`, format `date-time`)
- `updatedBy` (`AuditActor`)
- `lastChangeAt` (`string`, nullable, format `date-time`)
- `lastChangeType` (`string`, nullable)

#### `ApiKeyActivityEntry`

`object`

- `object` (`string`, one of `"api_key_event"`)
- `id` (`string`)
- `keyId` (`string`)
- `keyName` (`string`): `Deleted key` once the key itself is gone.
- `type` (`string`, one of `"created"`, `"updated"`, `"rotated"`, `"revoked"`, `"deactivated"`, `"reactivated"`, `"auth_failed"`, `"deleted"`)
- `createdAt` (`string`, format `date-time`)
- `actor` (`AuditActor`)
- `detail` (`object`): What changed and where it was changed from (`source`: console, api, mcp or documentation). A refused authentication carries `reason`.

#### `ApiKeyActivityList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`ApiKeyActivityEntry[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `ApiKeyList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`ApiKey[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `ApiKeyRequest`

`object`

One authenticated call a key made. Never a body or a query string.

- `object` (`string`, one of `"api_key_request"`)
- `id` (`string`)
- `keyId` (`string`)
- `keyName` (`string`): `Deleted key` once the key itself is gone.
- `requestId` (`string`, nullable): The `X-Request-Id` the call was answered with.
- `method` (`string`)
- `path` (`string`)
- `status` (`integer`)
- `errorCode` (`string`, nullable)
- `durationMs` (`integer`)
- `ip` (`string`, nullable)
- `userAgent` (`string`, nullable)
- `createdAt` (`string`, format `date-time`)

#### `ApiKeyRequestList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`ApiKeyRequest[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `ApiKeyStats`

`object`

- `object` (`string`, required, one of `"api_key_stats"`)
- `since` (`string`, required, format `date-time`)
- `until` (`string`, required, format `date-time`)
- `grain` (`string`, required, one of `"minute"`, `"hour"`, `"day"`)
- `keyIds` (`string[]`, required): The keys asked for. Empty means every key you can see.
- `sends` (`object`, required): The mail the keys sent, with what became of it.
  - `days` (`object[]`)
    - `day` (`string`): The bucket, shaped by `grain`.
    - `sent` (`integer`)
    - `delivered` (`integer`)
    - `failed` (`integer`)
    - `bounced` (`integer`)
    - `complained` (`integer`)
  - `totals` (`object`)
    - `sends` (`integer`)
    - `testSends` (`integer`)
    - `recipients` (`integer`)
    - `delivered` (`integer`)
    - `failed` (`integer`)
    - `bounced` (`integer`)
    - `complained` (`integer`)
    - `uncertain` (`integer`)
    - `pending` (`integer`)
  - `statuses` (`object[]`): Sends by status.
    - `label` (`string`)
    - `count` (`integer`)
  - `sources` (`object[]`): Sends by where they came from.
    - `label` (`string`)
    - `count` (`integer`)
  - `senders` (`object[]`): Sends by the address they went out as.
    - `label` (`string`)
    - `count` (`integer`)
  - `suppressed` (`object[]`): Recipients skipped because they were suppressed, by reason.
    - `label` (`string`)
    - `count` (`integer`)
- `rejected` (`object[]`, required): Calls refused because the secret was wrong, revoked or expired, per bucket.
  - `bucket` (`string`)
  - `count` (`integer`)
- `requests` (`object[]`, required): Requests made and how many of them failed with a 4xx or 5xx, per bucket.
  - `bucket` (`string`)
  - `count` (`integer`)
  - `failed` (`integer`)
- `routes` (`object[]`, required): The routes called most, with how many failed and the median time in milliseconds.
  - `label` (`string`)
  - `count` (`integer`)
  - `failed` (`integer`)
  - `medianMs` (`integer`)
- `codes` (`object[]`, required): Requests by the HTTP status they got back.
  - `label` (`string`)
  - `count` (`integer`)

#### `AuditActor`

`object`

Who made the change: a person, or a key acting over the API. Null when OpenEmail made it on its own, such as switching a webhook off after 100 failed events in a row, and when the person or key has since been deleted.

nullable

- `kind` (`string`, one of `"user"`, `"apiKey"`)
- `id` (`string`): The account id of the person, or the id of the key.
- `name` (`string`): The name of the person, or `API key <name>` for a key.
- `username` (`string`, nullable)
- `label` (`string`): What the app shows: `@username` for a person who has a username, otherwise `name`.
