# Per-tool keys

Create, list and revoke per-tool keys. A per-tool key is a normal kenari key that belongs to one tool on one machine, for example OpenCode on your laptop. If one tool's config leaks, you revoke that one key and the rest keep working. The endpoints are specific to kenari and have no OpenAI or Anthropic equivalent.

`POST /v1/account/keys`, `GET /v1/account/keys` and `DELETE /v1/account/keys/{id}`

## Who can call it

Only the key that `kenari login` saved can manage per-tool keys. See [kenari CLI](/en/docs/cli). Any other key gets `403` and `cli_login_key_required`, including:

- A key you created under **API Keys** in the dashboard, and the key from `kenari login --api-key`.
- A per-tool key. A tool cannot create more keys for itself.
- A key from `kenari login` that you created before per-tool keys existed. Run `kenari login` again to get one that works.

A shared API key gets `shared_key_not_allowed`, and a restricted key gets `restricted_key_not_allowed`. A key is restricted when it has a model restriction, a spend cap or a token cap. See [Account overview](/en/docs/account).

Each call needs the key in the `Authorization` header as `Bearer kn-...`.

## Create a key

`POST /v1/account/keys` creates a key under the key you call it with.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `tool` | string | Yes | The tool the key is for, such as `opencode`. 1 to 40 characters of `a-z`, `0-9`, `.` and `-`. |
| `host` | string | Yes | The machine the tool runs on, such as `laptop`. 1 to 64 characters of `A-Z`, `a-z`, `0-9`, `.`, `_` and `-`. |

The response is `201` with the new key:

```json
{
  "id": "e72755dd-ea6b-4d93-9e96-eb4c817824d0",
  "key": "kn-984e1408144f2e604c38d27e6596638ec57882ad022a4fdd",
  "prefix": "kn-984e1408",
  "label": "kenari-cli · opencode · laptop",
  "created_at": 1790000000
}
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | The id to use when you revoke the key. |
| `key` | string | The full key. It is shown once, so store it now. |
| `prefix` | string | The first characters of the key, to tell keys apart. |
| `label` | string | `kenari-cli · <tool> · <host>`. The same label shows next to the key under **API Keys** in the dashboard. |
| `created_at` | integer | When the key was created, in epoch seconds. |

A per-tool key works like the key it came from for every model and endpoint, except that it cannot manage keys. It follows the same billing as your other keys.

## List keys

`GET /v1/account/keys` takes no parameters. It returns the active per-tool keys of the key you call it with, newest first. Revoked keys are not listed, and the full key is never returned again.

```json
{
  "object": "list",
  "data": [
    {
      "id": "e72755dd-ea6b-4d93-9e96-eb4c817824d0",
      "prefix": "kn-984e1408",
      "label": "kenari-cli · opencode · laptop",
      "created_at": 1790000000,
      "last_used_at": 1790003600
    }
  ]
}
```

`last_used_at` is when the key last made a request, in epoch seconds. It is `null` when no use is recorded in the last 90 days.

## Revoke a key

`DELETE /v1/account/keys/{id}` revokes one per-tool key for good. The next request that uses it gets a `401`.

```json
{
  "id": "e72755dd-ea6b-4d93-9e96-eb4c817824d0",
  "revoked": true
}
```

The `id` must belong to a per-tool key of the key you call it with. Any other id returns `404` and `not_found`, whether the key does not exist, is already revoked, or belongs to someone else.

## Limits

- One login key can hold 30 active per-tool keys. Revoke one to create another.
- Per-tool keys count toward your account's key limit, the same as any other key.
- Each login key can create 20 keys per minute.

## Revoking the login key

When you revoke the key from `kenari login` under **API Keys** in the dashboard, all its per-tool keys are revoked with it. Revoking a single per-tool key does not affect the login key or the other per-tool keys. See [Authentication and API keys](/en/docs/authentication).

## Examples

### curl

```bash
# Create a key
curl https://kenari.id/v1/account/keys \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "opencode",
    "host": "laptop"
  }'

# List keys
curl https://kenari.id/v1/account/keys \
  -H "Authorization: Bearer $KENARI_API_KEY"

# Revoke a key
curl -X DELETE https://kenari.id/v1/account/keys/e72755dd-ea6b-4d93-9e96-eb4c817824d0 \
  -H "Authorization: Bearer $KENARI_API_KEY"
```

Here `$KENARI_API_KEY` is the key from `kenari login`.

### Python

```python
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['KENARI_API_KEY']}"}

created = requests.post(
    "https://kenari.id/v1/account/keys",
    headers=headers,
    json={"tool": "opencode", "host": "laptop"},
)
created.raise_for_status()
tool_key = created.json()
print(tool_key["label"])

requests.delete(
    f"https://kenari.id/v1/account/keys/{tool_key['id']}",
    headers=headers,
).raise_for_status()
```

### JavaScript

```javascript
const headers = { Authorization: `Bearer ${process.env.KENARI_API_KEY}` };

const created = await fetch("https://kenari.id/v1/account/keys", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ tool: "opencode", host: "laptop" }),
});
if (!created.ok) throw new Error(await created.text());
const toolKey = await created.json();
console.log(toolKey.label);

const revoked = await fetch(`https://kenari.id/v1/account/keys/${toolKey.id}`, {
  method: "DELETE",
  headers,
});
if (!revoked.ok) throw new Error(await revoked.text());
```

## Billing

Creating, listing and revoking keys is free. Requests made with a per-tool key are billed like any other request. See [How billing works](/en/docs/billing).

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `bad_request` | The body is not JSON with string fields `tool` and `host`, or one of them has characters or a length that is not allowed. Also when your account is at its key limit, with the message `key limit reached`. |
| 401 | None | The key is missing, invalid, revoked or expired. The body is a short plain-text message, not JSON. |
| 403 | `cli_login_key_required` | The key is not the one from `kenari login`. |
| 403 | `shared_key_not_allowed` | The key is a shared API key. |
| 403 | `restricted_key_not_allowed` | The key has a model restriction, a spend cap or a token cap. |
| 404 | `not_found` | On revoke, the id is not an active per-tool key of the key you called with. |
| 409 | `child_key_cap` | On create, the login key already holds 30 active per-tool keys. |
| 429 | `rate_limit_exceeded` | On create, more than 20 requests in one minute. `Retry-After` is 60 seconds. Wait that long and retry. |
| 500 | `internal_error` | Something failed in kenari. Retry once. |

See [Errors](/en/docs/errors) for the error format and the remaining codes.

## See also

- [Account overview](/en/docs/account)
- [kenari CLI](/en/docs/cli)
- [Authentication and API keys](/en/docs/authentication)
- [Errors](/en/docs/errors)
