---
title: "openemail.keys"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://openemail.uk/docs/python/reference/keys"
area: "Python"
category: "Reference"
---

# openemail.keys

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

Every key in the workspace: list, create, change, rotate, switch off and revoke them, never wider than the calling key, and read their request log and activity.

### `keys.list()`

List one page of the workspace's API keys

```python
def list(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[ApiKeyResource]
```

Returns one page of the workspace's keys, newest first, with what the Settings, API keys page shows: status, scopes, role, send scope, when each was last used, how often, and who made and last changed it. Revoked and expired keys stay listed until somebody deletes them. No secret is ever returned; `maskedKey` is enough to tell two keys apart.

A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other is a 404 rather than a refusal. Over OAuth only the workspace owner reaches it, and a member's token is 403 `owner_only`.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` from the previous page, passed back unchanged. Never build one yourself.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[ApiKeyResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item is an `ApiKeyResource`: `id`, `name`, `mode`, `maskedKey`, `keyLast4`, `status`, `scopes`, `roleId`, `roleName`, `addressAllowlist`, `domainAllowlist`, `expiresAt`, `lastUsedAt`, `totalUses`, `rotatedAt`, `rotationCount`, `deactivatedAt`, `revokedAt`, `revokedReason`, `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, `lastChangeAt` and `lastChangeType`. Never a secret.

**Example**

```python
from openemail import openemail

page = openemail.keys.list()

for key in page['items']:
    print(key['maskedKey'], key['name'], key['status'], key['lastUsedAt'])
```

**Notes**

- It needs `keys:read`, which no key holds unless somebody gave it one.
- The cursor is opaque: pass `nextCursor` back as it came.

Also available in: API [`GET /keys`](https://openemail.uk/docs/api/reference/api-keys#get-keys); TypeScript [`keys.list()`](https://openemail.uk/docs/sdk/reference/keys#list); Ruby [`keys.list`](https://openemail.uk/docs/ruby/reference/keys#list); CLI [`openemail keys list`](https://openemail.uk/docs/cli/reference/keys#keys-list).

### `keys.list_all()`

Collect every API key into one list

```python
def list_all(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[ApiKeyResource]
```

Walks every page of `list` and returns every key the caller can see, newest first, in one list.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[ApiKeyResource]`, newest first.

**Example**

```python
from openemail import openemail

keys = openemail.keys.list_all()

idle = [key['name'] for key in keys if key['status'] == 'active' and key['lastUsedAt'] is None]

print(idle)
```

**Notes**

- If any page fails, the call raises and the keys already fetched are discarded.

Also available in: API [`GET /keys`](https://openemail.uk/docs/api/reference/api-keys#get-keys); TypeScript [`keys.listAll()`](https://openemail.uk/docs/sdk/reference/keys#listAll); Ruby [`keys.list_all`](https://openemail.uk/docs/ruby/reference/keys#listAll).

### `keys.iterate()`

Stream the workspace's API keys one at a time

```python
def iterate(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[ApiKeyResource]
```

A generator over `list`, fetching a page only when the one before is drained. Nothing is requested until you start iterating, and breaking out of the loop stops the requests.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[ApiKeyResource]`, a generator yielding one key per step.

**Example**

```python
from datetime import datetime, timedelta, timezone

from openemail import openemail

soon = datetime.now(timezone.utc) + timedelta(days=7)

for key in openemail.keys.iterate():
    expires = key['expiresAt']

    if expires and datetime.fromisoformat(expires.replace('Z', '+00:00')) < soon:
        print(key['name'], 'expires soon')
```

Also available in: API [`GET /keys`](https://openemail.uk/docs/api/reference/api-keys#get-keys); TypeScript [`keys.iterate()`](https://openemail.uk/docs/sdk/reference/keys#iterate); Ruby [`keys.iterate`](https://openemail.uk/docs/ruby/reference/keys#iterate).

### `keys.get()`

Read one API key, without its secret

```python
def get(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> ApiKeyResource
```

Returns one key as `list` shows it. A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other is a 404 rather than a refusal, which `is_not_found` reports on the error. Over OAuth only the workspace owner reaches it, and a member's token is 403 `owner_only`.

`openemail.me.get()` describes the calling key itself and needs no scope; this reads any key the caller can see.

Scopes: `keys:read`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`ApiKeyResource`: `id`, `name`, `mode`, `maskedKey`, `keyLast4`, `status`, `scopes`, `roleId`, `roleName`, `addressAllowlist`, `domainAllowlist`, `expiresAt`, `lastUsedAt`, `totalUses`, `rotatedAt`, `rotationCount`, `deactivatedAt`, `revokedAt`, `revokedReason`, `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, `lastChangeAt` and `lastChangeType`. Never a secret.

**Example**

```python
from openemail import OpenEmailApiError, openemail

try:
    key = openemail.keys.get('4c1b257a66287fd113bd89d0')
except OpenEmailApiError as error:
    if not error.is_not_found:
        raise

    print('No such key, or one this key cannot see')
else:
    print(key['status'], key['scopes'], key['domainAllowlist'])
```

Also available in: API [`GET /keys/{id}`](https://openemail.uk/docs/api/reference/api-keys#get-keys-id); TypeScript [`keys.get()`](https://openemail.uk/docs/sdk/reference/keys#get); Ruby [`keys.get`](https://openemail.uk/docs/ruby/reference/keys#get); CLI [`openemail keys get`](https://openemail.uk/docs/cli/reference/keys#keys-get).

### `keys.create()`

Mint a new API key and receive its secret once

```python
def create(
    body: ApiKeyCreate,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> CreatedApiKeyResource
```

Creates a live key and returns it plus `token`, the whole secret. That is the only time it appears, 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 (none means every address the workspace owns), and the expiry is the caller's own or none. The workspace cap on live keys applies, as a 422 `workspace_limit_reached`.

A key never makes or reaches a key wider than itself. The target has to sit inside the caller 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 one address never covers its whole domain. Anything wider is 403 `beyond_caller_authority`, and the error's `param` names the axis.

Step-up verification, which the app asks for before it mints a key, cannot apply to a call made with a key, so `keys:manage` is a credential that makes credentials. Give it only to automation that provisions keys, narrow that key to the role and send scope it needs, and give it an expiry.

Scopes: `keys:manage`.

**Parameters**

- `body['name']` (`str`, required): A name, 1 to 60 characters.
- `body['scopes']` (`list[ApiScope]`): The scopes the key holds, at least one, each held by the caller. Defaults to `['emails:send']`.
- `body['roleId']` (`str | None`): A role to cap the key. Left out, the caller's own role. A caller with a role can only give its own, and `None` is refused for it.
- `body['addressAllowlist']` (`list[str]`): Single addresses the key may send as, at most 50, each owned by the workspace.
- `body['domainAllowlist']` (`list[str]`): Whole domains the key may send as, at most 25, including addresses added to them later. Leave both lists out to inherit the caller's own; send both empty for none.
- `body['expiresInMinutes']` (`int`): Minutes until the key expires, 5 to 5,256,000 (ten years). Left out, the caller's own expiry.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`CreatedApiKeyResource`: the `ApiKeyResource` fields `id`, `name`, `mode`, `maskedKey`, `keyLast4`, `status`, `scopes`, `roleId`, `roleName`, `addressAllowlist`, `domainAllowlist`, `expiresAt`, `lastUsedAt`, `totalUses`, `rotatedAt`, `rotationCount`, `deactivatedAt`, `revokedAt`, `revokedReason`, `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, `lastChangeAt` and `lastChangeType`, plus `token`, `oe_live_` followed by the id, an underscore and the secret.

**Example**

```python
from openemail import openemail

key = openemail.keys.create(
    {
        'name': 'Billing sender',
        'scopes': ['emails:send', 'emails:read'],
        'domainAllowlist': ['billing.acme.com'],
        'expiresInMinutes': 60 * 24 * 90,
    }
)

print(key['maskedKey'], key['expiresAt'])
print('Store this secret now, it is never shown again:', key['token'])
```

**Notes**

- Not retried automatically: a retry after a lost response would mint a second key.
- The new key is recorded in the activity log as made by the calling key, with `'api'` as the `source` in its `detail`.
- The success status is 201.

Also available in: API [`POST /keys`](https://openemail.uk/docs/api/reference/api-keys#post-keys); TypeScript [`keys.create()`](https://openemail.uk/docs/sdk/reference/keys#create); Ruby [`keys.create`](https://openemail.uk/docs/ruby/reference/keys#create); CLI [`openemail keys create`](https://openemail.uk/docs/cli/reference/keys#keys-create).

### `keys.update()`

Rename a key, change its scopes or send scope, or switch it off and on

```python
def update(
    id: str,
    patch: ApiKeyPatch,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> ApiKeyResource
```

Applies a partial change and returns the key as it now stands. `scopes`, `addressAllowlist` and `domainAllowlist` REPLACE what the key had, and a field left out stays as it was. `'enabled': False` switches the key off: every call with it is refused with `inactive_api_key` and it keeps its secret, scopes, role and send scope, so `'enabled': True` restores it exactly. That is the reversible alternative to `revoke`. A revoked key cannot be changed, and answers 409 `revoked`.

A key never makes or reaches a key wider than itself. The target has to sit inside the caller 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 one address never covers its whole domain. Anything wider is 403 `beyond_caller_authority`, and the error's `param` names the axis. A key changing itself may only narrow itself.

A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other is a 404 rather than a refusal. Over OAuth only the workspace owner reaches it, and a member's token is 403 `owner_only`.

Scopes: `keys:manage`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `patch['name']` (`str`): A new name, 1 to 60 characters.
- `patch['scopes']` (`list[ApiScope]`): The whole new list of scopes, at least one.
- `patch['addressAllowlist']` (`list[str]`): The whole new list of single addresses.
- `patch['domainAllowlist']` (`list[str]`): The whole new list of whole domains.
- `patch['enabled']` (`bool`): `False` switches the key off, `True` switches it back on.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`ApiKeyResource`: `id`, `name`, `mode`, `maskedKey`, `keyLast4`, `status`, `scopes`, `roleId`, `roleName`, `addressAllowlist`, `domainAllowlist`, `expiresAt`, `lastUsedAt`, `totalUses`, `rotatedAt`, `rotationCount`, `deactivatedAt`, `revokedAt`, `revokedReason`, `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, `lastChangeAt` and `lastChangeType`. Never a secret.

**Example**

```python
from openemail import openemail

openemail.keys.update('4c1b257a66287fd113bd89d0', {'enabled': False})

key = openemail.keys.update('4c1b257a66287fd113bd89d0', {'name': 'Billing sender (paused)'})

print(key['status'], key['deactivatedAt'])
```

**Notes**

- An empty patch is a 422. The call is retried on network failure and retryable statuses, because applying the same patch twice leaves the same key.

Also available in: API [`PATCH /keys/{id}`](https://openemail.uk/docs/api/reference/api-keys#patch-keys-id); TypeScript [`keys.update()`](https://openemail.uk/docs/sdk/reference/keys#update); Ruby [`keys.update`](https://openemail.uk/docs/ruby/reference/keys#update); CLI [`openemail keys update`](https://openemail.uk/docs/cli/reference/keys#keys-update).

### `keys.delete()`

Remove a revoked key from the list

```python
def delete(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> DeletedApiKeyResource
```

Deletes a key that has already been revoked. Its request log and activity stay, under `Deleted key`, so the history of what it did is not lost with it. A key that has not been revoked is refused with 409 `not_revoked`, so nothing still calling with it loses its credential without somebody deciding that first.

A key never makes or reaches a key wider than itself. The target has to sit inside the caller 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 one address never covers its whole domain. Anything wider is 403 `beyond_caller_authority`, and the error's `param` names the axis.

Scopes: `keys:manage`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`DeletedApiKeyResource`, a dict with `'object': 'api_key'`, the `id` and `'deleted': True`.

**Example**

```python
from openemail import openemail

revoked = openemail.keys.revoke('4c1b257a66287fd113bd89d0', {'reason': 'Leaked in a build log'})

openemail.keys.delete(revoked['id'])

print('Deleted', revoked['name'], 'after revoking it at', revoked['revokedAt'])
```

**Notes**

- Not retried automatically. If you repeat it yourself after a lost response, a 404 means the first attempt already worked.

Also available in: API [`DELETE /keys/{id}`](https://openemail.uk/docs/api/reference/api-keys#delete-keys-id); TypeScript [`keys.delete()`](https://openemail.uk/docs/sdk/reference/keys#delete); Ruby [`keys.delete`](https://openemail.uk/docs/ruby/reference/keys#delete); CLI [`openemail keys delete`](https://openemail.uk/docs/cli/reference/keys#keys-delete).

### `keys.rotate()`

Give a key a new secret and receive it once

```python
def rotate(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RotatedApiKeyResource
```

Mints a new secret for a key and returns the key plus `token`, `rotationCount` and `rotatedAt`. The id, name, scopes, role, send scope, expiry and request history all carry on; only the secret and `keyLast4` change. There is no overlap window: the old secret stops working the instant this returns.

Rotating the calling key itself is what `openemail.me.rotate()` does, and here it is allowed with `keys:write` as well as `keys:manage`. A revoked or expired key cannot be rotated, and answers 409 `revoked` or `expired`.

A key never makes or reaches a key wider than itself. The target has to sit inside the caller 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 one address never covers its whole domain. Anything wider is 403 `beyond_caller_authority`, and the error's `param` names the axis. Rotation hands the caller a working secret for the key, which is why the ceiling is checked against the key as it stands.

A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other is a 404 rather than a refusal. Over OAuth only the workspace owner reaches it, and a member's token is 403 `owner_only`.

Scopes: `keys:manage`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RotatedApiKeyResource`: the `ApiKeyResource` fields `id`, `name`, `mode`, `maskedKey`, `keyLast4`, `status`, `scopes`, `roleId`, `roleName`, `addressAllowlist`, `domainAllowlist`, `expiresAt`, `lastUsedAt`, `totalUses`, `rotatedAt`, `rotationCount`, `deactivatedAt`, `revokedAt`, `revokedReason`, `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, `lastChangeAt` and `lastChangeType`, plus the new secret in `token`. `rotationCount` and `rotatedAt` already count this rotation.

**Example**

```python
from openemail import openemail

rotated = openemail.keys.rotate('4c1b257a66287fd113bd89d0')

print(rotated['rotationCount'], rotated['rotatedAt'])
print('Store the new secret now, the old one has stopped working:', rotated['token'])
```

**Notes**

- Not retried automatically. Repeating a rotation would invalidate the secret the first attempt returned.

Also available in: API [`POST /keys/{id}/rotate`](https://openemail.uk/docs/api/reference/api-keys#post-keys-id-rotate); TypeScript [`keys.rotate()`](https://openemail.uk/docs/sdk/reference/keys#rotate); Ruby [`keys.rotate`](https://openemail.uk/docs/ruby/reference/keys#rotate); CLI [`openemail keys rotate`](https://openemail.uk/docs/cli/reference/keys#keys-rotate).

### `keys.revoke()`

Revoke a key for good

```python
def revoke(
    id: str,
    body: ApiKeyRevoke | None = None,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> ApiKeyResource
```

Revokes a key: every later call with it is refused with `revoked_api_key`, and it can never be switched back on, rotated or changed. `reason` is kept on the key and in the activity log. Revoking a key that is already revoked changes nothing and returns it as it is. A key may revoke itself, which is how an integration that believes its secret leaked retires it at once.

A key never makes or reaches a key wider than itself. The target has to sit inside the caller 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 one address never covers its whole domain. Anything wider is 403 `beyond_caller_authority`, and the error's `param` names the axis.

A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other is a 404 rather than a refusal. Over OAuth only the workspace owner reaches it, and a member's token is 403 `owner_only`.

Scopes: `keys:manage`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `body['reason']` (`str`): Why, at most 200 characters. The body is optional.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`ApiKeyResource`: `id`, `name`, `mode`, `maskedKey`, `keyLast4`, `status`, `scopes`, `roleId`, `roleName`, `addressAllowlist`, `domainAllowlist`, `expiresAt`, `lastUsedAt`, `totalUses`, `rotatedAt`, `rotationCount`, `deactivatedAt`, `revokedAt`, `revokedReason`, `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, `lastChangeAt` and `lastChangeType`. Never a secret. `status` is `'revoked'`.

**Example**

```python
from openemail import openemail

key = openemail.keys.revoke('4c1b257a66287fd113bd89d0', {'reason': 'Contractor offboarded'})

print(key['status'], key['revokedAt'], key['revokedReason'])
```

**Notes**

- Retried on network failure and retryable statuses, because revoking twice leaves the same key. Prefer `update(id, {'enabled': False})` while you find out whether anything still depends on a key.

Also available in: API [`POST /keys/{id}/revoke`](https://openemail.uk/docs/api/reference/api-keys#post-keys-id-revoke); TypeScript [`keys.revoke()`](https://openemail.uk/docs/sdk/reference/keys#revoke); Ruby [`keys.revoke`](https://openemail.uk/docs/ruby/reference/keys#revoke); CLI [`openemail keys revoke`](https://openemail.uk/docs/cli/reference/keys#keys-revoke).

### `keys.list_requests()`

List one page of one key's request log

```python
def list_requests(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    failed_only: bool | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[ApiKeyRequestResource]
```

Returns one page of the calls one key made, newest first, the Requests tab of the key in the app. The request log records every authenticated call a key made: method, path, status, error code, duration, IP and user agent, and never a body or a query string. A call refused before a key could be identified is not in it, and neither is a call made with an OAuth access token. Nothing is pruned, so the log reaches back to a key's first call. `failed_only=`, `since=` and `until=` are the filters the app offers.

A deleted key's log stays readable to a key that is not narrowed. A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other is a 404 rather than a refusal. Over OAuth only the workspace owner reaches it, and a member's token is 403 `owner_only`.

Scopes: `keys:read`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `limit` (`int`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` from the previous page, passed back unchanged. Never build one yourself.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `failed_only` (`bool`): Only calls answered with a status of 400 or more.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[ApiKeyRequestResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item has `id`, `keyId`, `keyName`, `requestId`, `method`, `path`, `status`, `errorCode`, `durationMs`, `ip`, `userAgent` and `createdAt`.

**Example**

```python
from openemail import openemail

page = openemail.keys.list_requests('4c1b257a66287fd113bd89d0', failed_only=True, limit=50)

for call in page['items']:
    print(call['createdAt'], call['method'], call['path'], call['status'], call['errorCode'])
```

**Notes**

- The cursor is opaque and stays valid under the same filters. One this log did not hand out is a 400 `invalid_cursor`.

Also available in: API [`GET /keys/{id}/requests`](https://openemail.uk/docs/api/reference/api-keys#get-keys-id-requests); TypeScript [`keys.listRequests()`](https://openemail.uk/docs/sdk/reference/keys#listRequests); Ruby [`keys.list_requests`](https://openemail.uk/docs/ruby/reference/keys#listRequests); CLI [`openemail keys list-requests`](https://openemail.uk/docs/cli/reference/keys#keys-list-requests).

### `keys.list_all_requests()`

Collect one key's whole request log into one list

```python
def list_all_requests(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    failed_only: bool | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[ApiKeyRequestResource]
```

Walks every page of `list_requests` under the same filters and returns every call in one list. The log is never pruned, so give it a window unless you mean to read a busy key's whole history, or use `iterate_requests` to stop early.

Scopes: `keys:read`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `limit` (`int`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `failed_only` (`bool`): Only calls answered with a status of 400 or more.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[ApiKeyRequestResource]`, newest first.

**Example**

```python
from datetime import datetime, timedelta, timezone

from openemail import openemail

today = openemail.keys.list_all_requests(
    '4c1b257a66287fd113bd89d0',
    since=datetime.now(timezone.utc) - timedelta(days=1),
    limit=100,
)

print(len(today), 'calls in the last 24 hours')
```

**Notes**

- If any page fails, the call raises and the rows already fetched are discarded.

Also available in: API [`GET /keys/{id}/requests`](https://openemail.uk/docs/api/reference/api-keys#get-keys-id-requests); TypeScript [`keys.listAllRequests()`](https://openemail.uk/docs/sdk/reference/keys#listAllRequests); Ruby [`keys.list_all_requests`](https://openemail.uk/docs/ruby/reference/keys#listAllRequests).

### `keys.iterate_requests()`

Stream one key's request log one call at a time

```python
def iterate_requests(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    failed_only: bool | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[ApiKeyRequestResource]
```

A generator over `list_requests` under the same filters, fetching a page only when the one before is drained. Nothing is requested until you start iterating, and breaking out of the loop stops the requests.

Scopes: `keys:read`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `limit` (`int`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `failed_only` (`bool`): Only calls answered with a status of 400 or more.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[ApiKeyRequestResource]`, a generator yielding one call per step.

**Example**

```python
from openemail import openemail

for call in openemail.keys.iterate_requests('4c1b257a66287fd113bd89d0', failed_only=True):
    if call['errorCode'] == 'from_address_forbidden':
        print('First refused send at', call['createdAt'])
        break
```

Also available in: API [`GET /keys/{id}/requests`](https://openemail.uk/docs/api/reference/api-keys#get-keys-id-requests); TypeScript [`keys.iterateRequests()`](https://openemail.uk/docs/sdk/reference/keys#iterateRequests); Ruby [`keys.iterate_requests`](https://openemail.uk/docs/ruby/reference/keys#iterateRequests).

### `keys.list_activity()`

List one page of what happened to one key

```python
def list_activity(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[ApiKeyActivityResource]
```

Returns one page of one key's audit log, newest first, the Activity tab of the key in the app. Every change to a key is a row: `created`, `updated`, `rotated`, `deactivated`, `reactivated`, `revoked` and `deleted`, plus `auth_failed` for every call that presented the key and was refused. `actor` names who made the change, a person as `@username` or a key as `API key <name>` in `label`, and `detail['source']` says where it came from: `console`, `api`, `mcp` or `documentation`.

A deleted key keeps its history, readable to a key that is not narrowed. A key narrowed to some domains or addresses only sees the keys whose send scope sits inside its own, so any other is a 404 rather than a refusal. Over OAuth only the workspace owner reaches it, and a member's token is 403 `owner_only`.

Scopes: `keys:read`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `limit` (`int`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` from the previous page, passed back unchanged. Never build one yourself.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[ApiKeyActivityResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item has `id`, `keyId`, `keyName`, `type`, `createdAt`, `actor` and `detail`.

**Example**

```python
from openemail import openemail

page = openemail.keys.list_activity('4c1b257a66287fd113bd89d0')

for change in page['items']:
    actor = change['actor']['label'] if change['actor'] else 'unknown'

    print(change['createdAt'], change['type'], actor, change['detail'].get('source'))
```

Also available in: API [`GET /keys/{id}/activity`](https://openemail.uk/docs/api/reference/api-keys#get-keys-id-activity); TypeScript [`keys.listActivity()`](https://openemail.uk/docs/sdk/reference/keys#listActivity); Ruby [`keys.list_activity`](https://openemail.uk/docs/ruby/reference/keys#listActivity); CLI [`openemail keys list-activity`](https://openemail.uk/docs/cli/reference/keys#keys-list-activity).

### `keys.list_all_activity()`

Collect one key's whole audit log into one list

```python
def list_all_activity(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[ApiKeyActivityResource]
```

Walks every page of `list_activity` under the same window and returns every change in one list, newest first.

Scopes: `keys:read`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `limit` (`int`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[ApiKeyActivityResource]`, newest first.

**Example**

```python
from openemail import openemail

history = openemail.keys.list_all_activity('4c1b257a66287fd113bd89d0')

refused = [change for change in history if change['type'] == 'auth_failed']

print(len(refused), 'refused attempts')
```

**Notes**

- If any page fails, the call raises and the rows already fetched are discarded.

Also available in: API [`GET /keys/{id}/activity`](https://openemail.uk/docs/api/reference/api-keys#get-keys-id-activity); TypeScript [`keys.listAllActivity()`](https://openemail.uk/docs/sdk/reference/keys#listAllActivity); Ruby [`keys.list_all_activity`](https://openemail.uk/docs/ruby/reference/keys#listAllActivity).

### `keys.iterate_activity()`

Stream one key's audit log one change at a time

```python
def iterate_activity(
    id: str,
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[ApiKeyActivityResource]
```

A generator over `list_activity` under the same window, fetching a page only when the one before is drained. Nothing is requested until you start iterating, and breaking out of the loop stops the requests.

Scopes: `keys:read`.

**Parameters**

- `id` (`str`, required): Key id, the 24 hex characters after `oe_live_`.
- `limit` (`int`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[ApiKeyActivityResource]`, a generator yielding one change per step.

**Example**

```python
from openemail import openemail

for change in openemail.keys.iterate_activity('4c1b257a66287fd113bd89d0'):
    if change['type'] == 'rotated':
        actor = change['actor']['label'] if change['actor'] else 'unknown'

        print('Last rotated by', actor, 'at', change['createdAt'])
        break
```

Also available in: API [`GET /keys/{id}/activity`](https://openemail.uk/docs/api/reference/api-keys#get-keys-id-activity); TypeScript [`keys.iterateActivity()`](https://openemail.uk/docs/sdk/reference/keys#iterateActivity); Ruby [`keys.iterate_activity`](https://openemail.uk/docs/ruby/reference/keys#iterateActivity).

### `keys.list_workspace_requests()`

List one page of the request log of every key

```python
def list_workspace_requests(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    failed_only: bool | None = None,
    key_ids: Sequence[str] | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[ApiKeyRequestResource]
```

Returns one page of every call the workspace's keys made, newest first, the Requests tab of Settings, API keys. The request log records every authenticated call a key made: method, path, status, error code, duration, IP and user agent, and never a body or a query string. A call refused before a key could be identified is not in it, and neither is a call made with an OAuth access token. Nothing is pruned, so the log reaches back to a key's first call. `key_ids=`, `failed_only=`, `since=` and `until=` are the filters the app offers, and `key_ids=` may name a deleted key.

A narrowed key reads only the log of the keys it can see, so naming another key in `key_ids=` matches nothing.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` from the previous page, passed back unchanged. Never build one yourself.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `failed_only` (`bool`): Only calls answered with a status of 400 or more.
- `key_ids` (`Sequence[str]`): Key ids to read, at most 50, sent comma-separated. Left out, every key the caller can see.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[ApiKeyRequestResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item has `id`, `keyId`, `keyName`, `requestId`, `method`, `path`, `status`, `errorCode`, `durationMs`, `ip`, `userAgent` and `createdAt`.

**Example**

```python
from openemail import openemail

page = openemail.keys.list_workspace_requests(failed_only=True, since='2026-09-22T00:00:00Z')

for call in page['items']:
    print(call['keyName'], call['method'], call['path'], call['status'])
```

Also available in: API [`GET /keys/requests`](https://openemail.uk/docs/api/reference/api-keys#get-keys-requests); TypeScript [`keys.listWorkspaceRequests()`](https://openemail.uk/docs/sdk/reference/keys#listWorkspaceRequests); Ruby [`keys.list_workspace_requests`](https://openemail.uk/docs/ruby/reference/keys#listWorkspaceRequests); CLI [`openemail keys list-workspace-requests`](https://openemail.uk/docs/cli/reference/keys#keys-list-workspace-requests).

### `keys.list_all_workspace_requests()`

Collect the request log of every key into one list

```python
def list_all_workspace_requests(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    failed_only: bool | None = None,
    key_ids: Sequence[str] | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[ApiKeyRequestResource]
```

Walks every page of `list_workspace_requests` under the same filters and returns every call in one list. The log is never pruned, so give it a window, or use `iterate_workspace_requests` to stop early.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `failed_only` (`bool`): Only calls answered with a status of 400 or more.
- `key_ids` (`Sequence[str]`): Key ids to read, at most 50, sent comma-separated. Left out, every key the caller can see.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[ApiKeyRequestResource]`, newest first.

**Example**

```python
from datetime import datetime, timedelta, timezone

from openemail import openemail

failures = openemail.keys.list_all_workspace_requests(
    failed_only=True,
    since=datetime.now(timezone.utc) - timedelta(hours=1),
    limit=100,
)

print(len(failures), 'failed calls in the last hour')
```

**Notes**

- If any page fails, the call raises and the rows already fetched are discarded.

Also available in: API [`GET /keys/requests`](https://openemail.uk/docs/api/reference/api-keys#get-keys-requests); TypeScript [`keys.listAllWorkspaceRequests()`](https://openemail.uk/docs/sdk/reference/keys#listAllWorkspaceRequests); Ruby [`keys.list_all_workspace_requests`](https://openemail.uk/docs/ruby/reference/keys#listAllWorkspaceRequests).

### `keys.iterate_workspace_requests()`

Stream the request log of every key one call at a time

```python
def iterate_workspace_requests(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    failed_only: bool | None = None,
    key_ids: Sequence[str] | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[ApiKeyRequestResource]
```

A generator over `list_workspace_requests` under the same filters, fetching a page only when the one before is drained. Nothing is requested until you start iterating, and breaking out of the loop stops the requests.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `failed_only` (`bool`): Only calls answered with a status of 400 or more.
- `key_ids` (`Sequence[str]`): Key ids to read, at most 50, sent comma-separated. Left out, every key the caller can see.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[ApiKeyRequestResource]`, a generator yielding one call per step.

**Example**

```python
from openemail import openemail

for call in openemail.keys.iterate_workspace_requests(failed_only=True):
    if call['status'] >= 500:
        print(call['requestId'], call['keyName'], call['path'])
```

Also available in: API [`GET /keys/requests`](https://openemail.uk/docs/api/reference/api-keys#get-keys-requests); TypeScript [`keys.iterateWorkspaceRequests()`](https://openemail.uk/docs/sdk/reference/keys#iterateWorkspaceRequests); Ruby [`keys.iterate_workspace_requests`](https://openemail.uk/docs/ruby/reference/keys#iterateWorkspaceRequests).

### `keys.list_workspace_activity()`

List one page of what happened to every key

```python
def list_workspace_activity(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    key_ids: Sequence[str] | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[ApiKeyActivityResource]
```

Returns one page of the audit log of every key in the workspace, newest first, the Activity tab of Settings, API keys. Every change to a key is a row: `created`, `updated`, `rotated`, `deactivated`, `reactivated`, `revoked` and `deleted`, plus `auth_failed` for every call that presented the key and was refused. `actor` names who made the change, a person as `@username` or a key as `API key <name>` in `label`, and `detail['source']` says where it came from: `console`, `api`, `mcp` or `documentation`.

`key_ids=` narrows it, deleted keys included, and `since=` and `until=` keep a window. A narrowed key reads only the activity of the keys it can see.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` from the previous page, passed back unchanged. Never build one yourself.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `key_ids` (`Sequence[str]`): Key ids to read, at most 50, sent comma-separated. Left out, every key the caller can see.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[ApiKeyActivityResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item has `id`, `keyId`, `keyName`, `type`, `createdAt`, `actor` and `detail`.

**Example**

```python
from openemail import openemail

page = openemail.keys.list_workspace_activity(since='2026-09-01T00:00:00Z')

for change in page['items']:
    actor = change['actor']['label'] if change['actor'] else 'unknown'

    print(change['keyName'], change['type'], actor)
```

Also available in: API [`GET /keys/activity`](https://openemail.uk/docs/api/reference/api-keys#get-keys-activity); TypeScript [`keys.listWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/keys#listWorkspaceActivity); Ruby [`keys.list_workspace_activity`](https://openemail.uk/docs/ruby/reference/keys#listWorkspaceActivity); CLI [`openemail keys list-workspace-activity`](https://openemail.uk/docs/cli/reference/keys#keys-list-workspace-activity).

### `keys.list_all_workspace_activity()`

Collect the audit log of every key into one list

```python
def list_all_workspace_activity(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    key_ids: Sequence[str] | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[ApiKeyActivityResource]
```

Walks every page of `list_workspace_activity` under the same filters and returns every change in one list, newest first.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `key_ids` (`Sequence[str]`): Key ids to read, at most 50, sent comma-separated. Left out, every key the caller can see.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[ApiKeyActivityResource]`, newest first.

**Example**

```python
from datetime import datetime, timezone

from openemail import openemail

changes = openemail.keys.list_all_workspace_activity(
    since=datetime(2026, 9, 1, tzinfo=timezone.utc)
)

over_api = [change for change in changes if change['detail'].get('source') == 'api']

print(len(over_api), 'changes made over the API')
```

**Notes**

- If any page fails, the call raises and the rows already fetched are discarded.

Also available in: API [`GET /keys/activity`](https://openemail.uk/docs/api/reference/api-keys#get-keys-activity); TypeScript [`keys.listAllWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/keys#listAllWorkspaceActivity); Ruby [`keys.list_all_workspace_activity`](https://openemail.uk/docs/ruby/reference/keys#listAllWorkspaceActivity).

### `keys.iterate_workspace_activity()`

Stream the audit log of every key one change at a time

```python
def iterate_workspace_activity(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    key_ids: Sequence[str] | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[ApiKeyActivityResource]
```

A generator over `list_workspace_activity` under the same filters, fetching a page only when the one before is drained. Nothing is requested until you start iterating, and breaking out of the loop stops the requests.

Scopes: `keys:read`.

**Parameters**

- `limit` (`int`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest row.
- `since` (`datetime | str`): Only rows at or after this instant. A `datetime` is sent as ISO 8601 in UTC, and a string must already be one.
- `until` (`datetime | str`): Only rows before this instant. It has to be later than `since`, or the server answers 400 `invalid_parameter`.
- `key_ids` (`Sequence[str]`): Key ids to read, at most 50, sent comma-separated. Left out, every key the caller can see.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[ApiKeyActivityResource]`, a generator yielding one change per step.

**Example**

```python
from openemail import openemail

for change in openemail.keys.iterate_workspace_activity(
    key_ids=['4c1b257a66287fd113bd89d0', '9e0f6b2c1d7a3e584b2c6f10']
):
    if change['type'] == 'auth_failed':
        print(change['keyName'], change['detail'].get('reason'), change['createdAt'])
```

Also available in: API [`GET /keys/activity`](https://openemail.uk/docs/api/reference/api-keys#get-keys-activity); TypeScript [`keys.iterateWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/keys#iterateWorkspaceActivity); Ruby [`keys.iterate_workspace_activity`](https://openemail.uk/docs/ruby/reference/keys#iterateWorkspaceActivity).

### `keys.stats()`

Read what the keys did inside a window

```python
def stats(
    *,
    since: datetime | str | None = None,
    until: datetime | str | None = None,
    key_ids: Sequence[str] | None = None,
    grain: TrackingGrain | None = None,
    offset_minutes: int | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> ApiKeyStatsResource
```

Returns the numbers behind the Analytics tab of the API keys page: the mail the keys sent and what became of it, the calls refused because a secret was wrong, revoked or expired, 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 covers every key you can see, or the ones `key_ids=` names. The window runs from `since=` to `until=`, and left out it is the 30 days before now. `grain=` sets the bucket width of the series and `offset_minutes=` shifts the boundaries so days break where the reader's day does.

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

**Parameters**

- `since` (`datetime | str`): The start of the window, a `datetime` or an ISO 8601 instant. Defaults to 30 days before `until`.
- `until` (`datetime | str`): The end of the window, not included. Defaults to now.
- `key_ids` (`Sequence[str]`): Only these keys, at most 50. Left out, every key you can see.
- `grain` (`TrackingGrain`): Bucket width: `'minute'`, `'hour'` or `'day'`, defaulting to `'day'`.
- `offset_minutes` (`int`): Minutes east of UTC to bucket in, from -840 to 840, defaulting to 0.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`ApiKeyStatsResource` with the window it covered, `sends`, `rejected`, `requests`, `routes` and `codes`.

**Example**

```python
from openemail import openemail

stats = openemail.keys.stats(grain='day', offset_minutes=60)

print(stats['sends']['totals']['sends'], 'sent since', stats['since'])

for route in stats['routes'][:3]:
    print(route['label'], route['count'], route['medianMs'])
```

**Notes**

- A key narrowed to some domains or addresses only counts the keys whose send scope sits inside its own, and an access token is refused with 403 `owner_only` unless it acts for the owner of the workspace.
- The series are sparse: a bucket with nothing in it has no entry, so a chart must fill the gaps.
- Retried automatically on network failure, since it only reads.

Also available in: API [`GET /keys/stats`](https://openemail.uk/docs/api/reference/api-keys#get-keys-stats); TypeScript [`keys.stats()`](https://openemail.uk/docs/sdk/reference/keys#stats); Ruby [`keys.stats`](https://openemail.uk/docs/ruby/reference/keys#stats); CLI [`openemail keys stats`](https://openemail.uk/docs/cli/reference/keys#keys-stats).
