> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traceten.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API keys API

> List, mint and revoke Traceten API keys programmatically.

## Base URL and authentication

```
https://api.traceten.com/v1/account/api-keys
```

Listing and revoking need `config:write` when you authenticate with an API key, listings included, because a list of every key on the account is a target list, not a metric. **Minting needs `credentials:write`**, which is a separate permission precisely so that a key managing your configuration cannot also issue new keys.

An AI app [connected to your account](/mcp/install) **cannot mint a key and cannot list your keys**: it is never granted `credentials:write`, and the listings are refused to it outright for the same reason: a connection that cannot issue a credential should not be able to inventory them.

It **can** revoke a key, if the person who approved the connection is an account-wide admin: revoking needs only `config:write`, which a connection can carry. In practice that is bounded by the listings being closed, so it has no way to discover a key id it was not given.

Dashboard sessions are unaffected: any member can still see the account's keys in **Settings → API keys**. The scope applies to keys only.

A key scoped to a single site cannot use the listings at all, because the rows name other sites.

Minting and revoking have two further rules, below.

<Warning>
  **Every API key issued before this release has been revoked.** You must create replacements before
  any of these endpoints will authenticate. See [re-minting your
  keys](/api/authentication#you-must-re-mint-every-existing-key).
</Warning>

## Before you mint a key with a key

Two things apply to every request that creates or revokes a credential here.

**1. A key cannot mint a key more capable than itself.** Every permission you request must be one the calling key already holds. If it is not, you get `403` and `required_scope` names the first one missing. We do not quietly issue a narrower key instead. See [minting credentials with a key](/api/authentication#minting-credentials-with-a-key).

**2. A key cannot mint one that outlives it.** If the calling key expires, the key it creates must expire no later. Requesting a later expiry, or none at all, is `403` with `error: "expiry_exceeds_parent"`. Otherwise a short-lived key lifted from a CI log could mint a permanent replacement of equal privilege and outlive its own revocation.

**3. The person behind your key must still be an account-wide admin.** We look up whoever created the calling key and check their current role. If they have been demoted or removed, the key stops working here. See [who is behind a key](/api/authentication#who-is-behind-a-key).

## When the person who created a key leaves

A key is tied to the team member who created it, and it does not outlive their place on your team.

* **When someone is removed from your organization**, every API key they created on that account is revoked as soon as we are notified of the removal.
* **When a person's user is deleted** from our authentication provider, every key they created is revoked on every account.

Revoked keys stay in the [revoked list](#get-v1-account-api-keys-revoked) as a record. API calls made with a revoked key get `401`. Our event-ingestion edge caches a valid key for up to a minute, so it can keep accepting events sent with a just-revoked key for about that long, and for up to 15 minutes if it cannot reach our database at that moment. If a script or CI job should survive someone leaving, have a person who is staying create its key.

## `GET /v1/account/api-keys`

Active keys, plus a count of revoked ones.

```bash theme={null}
curl https://api.traceten.com/v1/account/api-keys \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "keys": [
    {
      "id": "0c9d1a53-8e2f-4a71-b0c5-5f3a9e1d2b40",
      "prefix": "tk_live_example1",
      "name": "Production server",
      "last_used_at": "2026-08-28T14:02:19.221Z",
      "expires_at": null,
      "created_at": "2026-06-02T08:44:00.000Z",
      "site_id": null,
      "scopes": ["stats:read", "ingest:write"]
    }
  ],
  "revoked_count": 3
}
```

`prefix` is the first 16 characters of the key. It is not a secret; it is how you tell two keys apart in a list.

`site_id` is `null` for an account-wide key. Otherwise it is the snippet key (`ttid_…`) when you are using an API key, and the internal UUID when the dashboard reads this same endpoint with its own session.

**No listing ever returns key material.** The plaintext exists in exactly one response: the one that created it.

## `GET /v1/account/api-keys/revoked`

The same shape, for keys that have been revoked. Rows are kept as an audit record.

## `POST /v1/account/api-keys`

Mint a key.

```bash theme={null}
curl -X POST https://api.traceten.com/v1/account/api-keys \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Reporting bot",
    "scopes": ["stats:read"],
    "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12"
  }'
```

```json theme={null}
{
  "id": "3a7f0e21-4c88-4d9a-9b12-77a1f3c0e5d6",
  "prefix": "tk_live_example2",
  "name": "Reporting bot",
  "expires_at": null,
  "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12",
  "scopes": ["stats:read"],
  "plaintext_key": "tk_live_example2_not_a_real_key_do_not_use",
  "created_at": "2026-08-29T11:20:04.913Z"
}
```

| Field        | Type   | Required | Notes                                                                                     |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------------------- |
| `name`       | string | no       | Up to 80 characters.                                                                      |
| `scopes`     | array  | no       | Defaults to `["stats:read"]`. Must be a subset of the caller's own.                       |
| `site_id`    | string | no       | Restrict the key to one site. Snippet key (`ttid_…`) over the API. Omit for account-wide. |
| `expires_at` | string | no       | ISO-8601. Must be at least a minute in the future.                                        |

<Warning>
  `plaintext_key` is shown once and cannot be recovered. Store it before you discard the response.
  We keep only hashes.
</Warning>

An account can hold 50 active keys. Expired keys do not count.

## `DELETE /v1/account/api-keys/{keyId}`

Revoke a key. The row is kept as an audit record; only `revoked_at` is set.

```bash theme={null}
curl -X DELETE https://api.traceten.com/v1/account/api-keys/3a7f0e21-4c88-4d9a-9b12-77a1f3c0e5d6 \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "id": "3a7f0e21-4c88-4d9a-9b12-77a1f3c0e5d6",
  "revoked_at": "2026-08-29T11:31:47.002Z"
}
```

`keyId` is the `id` from the listing, not the prefix.

A key that does not exist, belongs to another account, or is already revoked all return the same `404`, so key ids cannot be discovered by guessing.

Revocation is immediate for this API. On the ingestion path it takes effect within about a minute; see [security](/api/authentication#security) for the one exception.

## Rotating a key

There is no rotate endpoint, on purpose. Rotation is three steps you control the timing of:

1. `POST /v1/account/api-keys` with the same permissions.
2. Deploy the new key everywhere it is used.
3. `DELETE /v1/account/api-keys/{oldKeyId}`.

Doing it in that order means there is never a moment when neither key works.

## Errors

| Status | `error`              | Cause                                                                                                                                                        |
| ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `401`  | `unauthorized`       | Missing, malformed, revoked or expired key.                                                                                                                  |
| `403`  | `insufficient_scope` | Missing `config:write` (listing, revoking) or `credentials:write` (minting), or requesting a permission the caller does not hold. `required_scope` names it. |
| `403`  | `forbidden`          | The person behind the key is no longer an account-wide admin, or the key is scoped to a single site.                                                         |
| `404`  | `not_found`          | Unknown key id, or a `site_id` this credential cannot reach.                                                                                                 |
| `422`  | `validation_failed`  | Bad body. `field` names the offending input.                                                                                                                 |
| `422`  | `key_limit_reached`  | 50 active keys already.                                                                                                                                      |
| `503`  | `clerk_unavailable`  | We could not verify the caller's permissions. Retry.                                                                                                         |

## Next

* [Bot tokens](/api/bot-tokens) for the crawl-report middleware
* [Permissions](/api/authentication#permissions) and what each one allows
