---
title: "API keys"
description: "`keys->list`, `listAll`, `iterate`, `get`, `create`, `update`, `delete`, `rotate` and `revoke`, and the request log, activity and stats readers."
url: "https://openemail.uk/docs/php/keys"
area: "PHP"
category: "Mailbox"
---

# API keys

`keys->list`, `listAll`, `iterate`, `get`, `create`, `update`, `delete`, `rotate` and `revoke`, and the request log, activity and stats readers.

## Every method

**keys.php**

```
use OpenEmail\Constants\ApiScopes;

$key = $client->keys->create([
    'name' => 'Billing sender',
    'scopes' => [ApiScopes::EMAILS_SEND],
    'domainAllowlist' => ['billing.acme.com'],
    'expiresInMinutes' => 60 * 24 * 90,
]);

file_put_contents('.openemail-billing-key', $key['token']);

$client->keys->update($key['id'], ['enabled' => false]);

$rotated = $client->keys->rotate($key['id']);
file_put_contents('.openemail-billing-key', $rotated['token']);

$client->keys->revoke($key['id'], ['reason' => 'Replaced']);
$client->keys->delete($key['id']);
```

`create` and `rotate` are the only calls that return a secret, in `token`, once. Every read returns `maskedKey` instead. `update` switches a key off and on with `enabled`, which is the reversible alternative to `revoke`, and `delete` only removes a key that has been revoked: any other key is a 409 `not_revoked`, thrown as a `ConflictException`. Reading needs `keys:read` and every change needs `keys:manage`.

`list` returns one `OpenEmail\Result\Page`, `listAll` returns every key in one array, and `iterate` returns a `Generator` that yields one key at a time. `create` and `update` take the body as one array under the API’s names, and `revoke` takes an optional array with `reason`. An OAuth access token reaches these calls only when the person who connected the app owns the workspace. Anyone else’s gets a `PermissionException` with `errorCode` set to `owner_only`.

> The package never retries `create` or `rotate`. A retry after a lost response would mint a second key, or invalidate the secret the first attempt returned. `update` and `revoke` are retried after a network failure, because repeating them leaves the same key, and `delete` is not.

## Never wider than the caller

A key never makes or reaches a key wider than itself. Scopes, role, expiry, mode and send scope all have to sit inside the calling key, or the call throws a `PermissionException` with `errorCode` set to `beyond_caller_authority`, and `param` names the axis. A key narrowed to some domains or addresses only sees the keys inside its own send scope. `rotate` on the calling key also works with `keys:write`, like `$client->me->rotate()`.

> Step-up verification 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, give that key a role, a send scope and an expiry, and watch `listWorkspaceActivity`, where everything it does is recorded against it.

## Request log and activity

**key_logs.php**

```
$failures = $client->keys->listRequests(
    '4c1b257a66287fd113bd89d0',
    failedOnly: true,
    since: new \DateTimeImmutable('-1 day'),
);
echo count($failures), PHP_EOL;

foreach ($client->keys->iterateWorkspaceActivity() as $change) {
    echo $change['keyName'], ' ', $change['type'], ' ', $change['actor']['label'] ?? 'system', PHP_EOL;
}
```

`listRequests` and `listActivity` read one key, and `listWorkspaceRequests` and `listWorkspaceActivity` read every key or the ones `keyIds:` names, as an array or one comma-separated string. Each has a `listAll` and an `iterate` twin beside it, such as `listAllRequests` and `iterateRequests`. They take `since:` and `until:` as a `DateTimeInterface` or an ISO 8601 string, and the request readers also take `failedOnly:`.

`stats` reads what the keys did inside one window: the mail they sent and what became of it, the calls refused, the requests they made and the routes they called. It takes the same `keyIds:`, `since:` and `until:`, plus `grain:` and `offsetMinutes:`, covers the 30 days before now when you name no window, and needs `emails:read` beside `keys:read`.

> `until:` has to be later than `since:`, or the call throws an `InvalidRequestException` with `errorCode` set to `invalid_parameter` and `param` set to `until`. A `DateTimeInterface` is sent in UTC, and a string is sent as written, so it has to be an ISO 8601 timestamp such as `2026-09-01T00:00:00Z`.
