---
title: API keys
description: List the API keys your organization holds and revoke one immediately — what a listing shows, what it deliberately never shows, and what to do the moment a key leaks.
order: 10
---

# API keys

Your organization's keys are yours to see and yours to kill. Listing shows you what exists; revoking takes effect on the revoked key's very next request. Neither call needs us, and neither has an office-hours dependency — which is the whole point, because the moment you need to revoke a key is never a convenient one.

Keys are still **issued** by us; see [Access](/access). What changed is that you no longer have to ask in order to look at them or to stop one.

`:org` is your organization's slug or its `org_...` id. A key can only address its own organization: any other value answers `404`, exactly as an organization that does not exist would.

## A key has leaked. Do this now.

```bash
# 1. find its id
curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/orgs/acme/keys"

# 2. kill it
curl -s -X DELETE "$DOCSARY_API/v1/orgs/acme/keys/key_9f2c..." \
  -H "authorization: Bearer $DOCSARY_KEY"
```

The revoked key is dead on its next request. Nothing else is affected: your other keys keep working, your sites keep serving, and no content is touched. Then ask us for a replacement key and update whatever was holding the old one.

If the leaked key is the **only** key you have, use it to revoke itself — that is allowed however narrow its scopes are. See [Revoking the key you are holding](#revoking-the-key-you-are-holding).

## List keys

```
GET /v1/orgs/:org/keys
```

```bash
curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/orgs/acme/keys"
```

```json
{
  "org": "acme",
  "keys": [
    {
      "id": "key_9f2c1d0a4b6e8f1c2d0a4b6e8f1c2d0a",
      "name": "ci",
      "scopes": "write",
      "created_at": 1756300000000,
      "last_used_at": 1756399000000,
      "revoked_at": null,
      "revoked": false
    },
    {
      "id": "key_3a7b5c9d1e2f3a7b5c9d1e2f3a7b5c9d",
      "name": "docs-linter",
      "scopes": "read",
      "created_at": 1756200000000,
      "last_used_at": null,
      "revoked_at": 1756350000000,
      "revoked": true
    }
  ]
}
```

Newest first. A read-only key may list — looking is what a read-only key is for.

| Field | Meaning |
| --- | --- |
| `id` | The `key_...` id. This is what you pass to revoke, and it is safe to write down |
| `name` | The name the key was issued under |
| `scopes` | `write` or `read` — see [Conventions](/api/conventions#authentication) |
| `created_at` | Millisecond timestamp of issue |
| `last_used_at` | Millisecond timestamp of the key's most recent request, or `null` if it has never been used |
| `revoked_at` | Millisecond timestamp of revocation, or `null` |
| `revoked` | `true` once revoked. A revoked key stays listed |

**Revoked keys stay in the list**, with the time they died. That is deliberate: the question you usually have is "is the thing still calling us the key I killed last week?", and a list that quietly forgot cannot answer it.

**`last_used_at` is the useful column.** A key with no `last_used_at` months after issue is a key nothing depends on, which makes it free to revoke. A key you thought was retired but which has a recent timestamp is worth understanding before you kill it.

### What a listing never contains

**Not the key.** Keys are stored only as a hash of the token, and the token itself is stored nowhere at all — it exists in exactly one response, the one that issued it. Nothing here can return it, including by mistake.

**Not the hash either.** The stored hash is what a request is checked against, so publishing it would turn this endpoint into a way to test guesses offline. It is never returned in any field.

A lost key cannot be recovered, only replaced. Revoke it and ask for a new one.

## Revoke a key

```
DELETE /v1/orgs/:org/keys/:id
```

```bash
curl -s -X DELETE "$DOCSARY_API/v1/orgs/acme/keys/key_9f2c..." \
  -H "authorization: Bearer $DOCSARY_KEY"
```

```json
{
  "revoked": true,
  "self": false,
  "key": {
    "id": "key_9f2c1d0a4b6e8f1c2d0a4b6e8f1c2d0a",
    "name": "ci",
    "scopes": "write",
    "created_at": 1756300000000,
    "last_used_at": 1756399000000,
    "revoked_at": 1756400000000,
    "revoked": true
  }
}
```

**Immediate.** Every request checks the key against the store as it arrives, so the revoked key gets `401` on its next call. There is no cache to wait out and no propagation window.

**Irreversible.** A revoked key cannot be un-revoked. Replacing it means issuing a new one.

**Idempotent.** Revoking an already-revoked key answers `200` with `"already_revoked": true`, not an error. Re-running the command you ran in a panic should not make you wonder whether the first run worked.

Revoking a key changes no content. Sites keep serving, pages are untouched, and your other keys keep working.

### Who may revoke

| Caller | May revoke |
| --- | --- |
| A write-capable key | Any key in its own organization |
| A read-only key | Only itself |

Revocation is a change, so a read-only key does not get to make it on somebody else's behalf — with one deliberate exception below.

### Revoking the key you are holding

**Any key may revoke itself, whatever its scopes.** If the credential that leaked is the read-only one, and it is the only one you have to hand, you can still stop it:

```bash
# whoami tells you which key you are holding
curl -s -H "authorization: Bearer $DOCSARY_KEY" "$DOCSARY_API/v1/whoami"
```

```json
{ "kind": "org", "org": { "slug": "acme" }, "scopes": "read", "key_id": "key_3a7b..." }
```

Passing that `key_id` to the revoke call works, and the response says so:

```json
{
  "revoked": true,
  "self": true,
  "note": "That was the key this request authenticated with. It is dead now."
}
```

The exception is safe because a key destroying only itself cannot be turned against anything else. The next request that key makes will fail, so make it the last thing you do with it.

### Errors

| Response | Cause |
| --- | --- |
| `401` | No token, or the token has already been revoked |
| `403` containing `read-only` | A read-only key tried to revoke a different key |
| `404 {"error":"key not found"}` | No such key id in this organization — including a real key id belonging to somebody else |
| `404 {"error":"org not found"}` | The `:org` value is not your organization |

Both `404`s are the same answer another organization's real slug gets, so neither can be used to find out what exists.

## Good practice

**One key per consumer.** A key for CI, a key for the docs linter, a key for the script on your laptop. Then revoking the one that leaked costs you exactly one thing to re-issue instead of taking down everything at once.

**Read-only wherever writing is not needed.** A linter, a status check, a dashboard: all of them only look. Ask for `read` scope and the blast radius of that key leaking is a `401` on a mutation.

**Check `last_used_at` occasionally.** It is the cheapest audit there is, and it turns "we should tidy up our keys some day" into a two-minute job with an obvious answer.

**Revoke first, investigate afterwards.** Revocation costs nothing, breaks nothing else, and cannot be got wrong in a way that matters. Working out how the key got out is a slower job and does not need to block the fast one.
