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

# Managing API Keys

> Create, rotate, and revoke automation keys — key format, expiry, grace windows, rate limits, and error codes.

Automation keys authenticate your scripts and CI against the VisIQ management
API. This page covers their full lifecycle. For what automation keys are and
how the permission model works, start with the
[Platform Automation introduction](/automation/introduction).

<Note>
  **Agents can obtain automation keys too.** Besides the dashboard flow below,
  an AI agent can self-register through the `agent_auth` device flow with
  `"requested_key_type": "management"` and an explicit permission list; a human
  reviews the exact grant at **`/agent/claim`** and approves or narrows it. The
  grant can never exceed the approver's own permissions. See the
  [agent authentication guide](https://visiqlabs.com/auth.md).
</Note>

***

## Create a key

1. In the dashboard, open **Settings → API Keys**.
2. Click **Create New Key** (requires the `api_keys:create` permission).
3. Fill in the dialog:
   * **Name** — a label for the key (1–50 characters), e.g. `ci-audit-export`.
   * **Environment** — `production` or `test`. This only determines the key
     prefix (`vq_prod_` vs `vq_test_`) so you can tell credentials apart.
   * **Expires** — `30 days`, `90 days` (default), `180 days`, `1 year`, or
     `No expiry`. Prefer an expiry; expired keys stop authenticating
     automatically.
   * **Permissions** — pick the explicit permissions the key needs, grouped
     the same way as the team Roles matrix. Each group has a select-all
     checkbox, and **Grant all** selects everything — use it sparingly.
4. The full key is displayed **exactly once** — copy it into your secrets
   manager immediately.

You can only grant permissions you hold yourself. A request for anything
beyond your own effective permissions is rejected with `403` and an
`exceededPermissions` list — no key can be more powerful than its creator.
Every creation is recorded in your audit log as `API_KEY_CREATED`, and the
operation aborts if the audit write fails.

<Warning>
  The plaintext key is shown only at creation (and rotation). VisIQ stores a
  SHA-256 hash — verified with a timing-safe comparison — never the key itself.
  If you lose it, rotate or re-create the key. Never commit keys to source
  control.
</Warning>

### Key format

```text theme={null}
vq_prod_f3a91c0e5b27d8146a0c9e3f71b52d80e4c6a1f97d3b08e25c41f6a890b7d213
vq_test_0d8e2a71c45f9b36e810d72c4a95f3081b6e0d49c27a85f1e3b09c64d2a7f581
```

A `vq_prod_` / `vq_test_` prefix followed by 64 hex characters. The dashboard
key table shows only the first 16 characters (`vq_prod_f3a91c0e****`) so you
can identify a key without exposing it.

***

## Use a key

Send it as a bearer token to `https://api.visiqlabs.com`:

```bash theme={null}
curl https://api.visiqlabs.com/v1/allow/audit-log \
  -H "Authorization: Bearer vq_prod_..."
```

The request succeeds when the route's required permission is in the key's
permission list — this example needs `allow_audit_log:view` — otherwise it is
denied (see [Errors](#errors)).

Every authenticated request updates the key's **Last Used** timestamp, shown
in the dashboard key table. Use it to spot stale keys and revoke them before
they become forgotten liabilities.

***

## Rotate a key

Rotation issues a **new secret** for the same logical key — name, environment,
permissions, audience, and expiry all carry over — and retires the old one.
In the key table, open the row's actions menu and choose **Rotate Key**
(requires `api_keys:rotate`).

The rotation dialog asks how long to **keep the old key valid** — the grace
window: `Revoke immediately`, `1 hour`, `24 hours` (default), or `7 days`.

* The new key is returned **once**, exactly like at creation.
* The **old key keeps authenticating until the grace window lapses**, so
  in-flight callers can cut over with zero downtime. The table shows the
  retiring row as `Grace ends in …`.
* With **Revoke immediately**, the old key stops authenticating at once.
* Once the grace window lapses the old key returns `401 API key has expired`.
* The retired key is linked to its replacement (`replaced_by_key_id`), so the
  rotation chain is auditable, and the rotation itself is logged as
  `API_KEY_ROTATED`. If the audit write fails, the new key is rolled back —
  a rotation never leaves an unaudited live secret behind.

<Note>
  Rotate-with-grace means **two** valid secrets exist for the duration of the
  window. Pick **Revoke immediately** when you suspect the old key is
  compromised — grace is for routine credential hygiene, not incident response.
</Note>

***

## Revoke a key

Revocation is available today, including for existing automation keys. Choose
**Delete Key** in the row's actions menu (requires `api_keys:delete`) and
confirm. Revocation is **immediate** — the very next request with that key
receives `401` — and is logged as `API_KEY_REVOKED`. There is no grace window
and no undo; create a new key if you revoked the wrong one.

***

## Revoke your own key (self-revocation)

Beyond the dashboard, **the holder of a key can always revoke that key itself** —
no dashboard, no human, and **no special permission** ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)
spirit). This is the clean way for an automation or agent to end its own session
or retire a credential it believes is compromised, and it is the only revoke
path a permission-scoped key can rely on (such a key may not hold
`api_keys:delete`).

The endpoint is authenticated by the **presenting key itself** — the credential
is resolved from its own bearer token, so the only key it can ever target is the
caller's own. There is no request body naming another key, which makes cross-key
revocation impossible through this path.

```bash theme={null}
# Works for a harness OR a management key
curl -X POST https://api.visiqlabs.com/allow/self/revoke \
  -H "Authorization: Bearer vq_prod_..."
```

```json theme={null}
{ "revoked": true, "api_key_id": "...", "key_type": "management" }
```

The same operation is exposed on the web JSON API at
`POST https://app.visiqlabs.com/api/allow/self/revoke`, and as the
[`revoke_self`](/automation/mcp) tool on the Platform MCP server (no arguments).

<Warning>
  Self-revocation is **immediate, idempotent, and irreversible**. After it
  succeeds the key stops authenticating at once, so it must be the **last call**
  you make with that key. A key that is already revoked, unknown, or expired
  returns `401`; a dashboard user session (not an API key) returns `400
    not_an_api_key`. Legacy vendor root keys have no `api_keys` row and are not
  self-revocable — rotate them from the dashboard instead.
</Warning>

Agents that obtained their key through the
[device flow](/automation/agent-device-flow) can also tear it down (including a
still-pending registration) via `POST /api/agent/identity/revoke`.

***

## Expiry

A key with an expiry stops authenticating the moment `expires_at` passes —
requests receive `401 API key has expired`. The dashboard shows expiry as
`in 12d` / `Never` / `Expired`. Expiry cannot be edited after creation, and a
rotated key's replacement **inherits the original expiry** — to extend a key's
life, create a new key.

***

## Rate limits

API-key requests are rate limited per key with a sliding window — **600
requests per minute** by default. Every authenticated response carries the
current state:

| Header                  | Meaning                                  |
| ----------------------- | ---------------------------------------- |
| `X-RateLimit-Limit`     | The per-window request limit             |
| `X-RateLimit-Remaining` | Requests remaining in the current window |

Exceeding the limit returns `429 Too Many Requests` with a `Retry-After`
header (seconds until the window frees up):

```json theme={null}
{
  "error": "rate_limited",
  "detail": "API key rate limit exceeded.",
  "retryAfter": 42
}
```

Back off for `Retry-After` seconds before retrying. Dashboard sessions are not
subject to this limit — it applies to API-key traffic only. The limiter is
fail-closed: if it cannot be evaluated, the request is denied rather than
waved through.

***

## Errors

| Status | Error                       | Cause                                                                         |
| ------ | --------------------------- | ----------------------------------------------------------------------------- |
| `401`  | `Invalid API key or token`  | The key is unknown, malformed, or revoked                                     |
| `401`  | `API key has expired`       | The key passed its expiry, or its rotation grace window lapsed                |
| `403`  | `insufficient_permission`   | The key's permission list does not contain the permission this route requires |
| `403`  | `insufficient_scope`        | The key's coarse scopes do not cover this scope-gated route                   |
| `403`  | `harness_key_not_permitted` | A harness key was used against a management endpoint — use an automation key  |
| `429`  | `rate_limited`              | Per-key rate limit exceeded — honor `Retry-After`                             |

A `403 insufficient_permission` response tells you exactly what was missing:

```json theme={null}
{
  "error": "insufficient_permission",
  "detail": "This management API key is not granted the requested permission.",
  "requiredPermission": "allow_rules:update",
  "grantedPermissions": ["allow_rules:view", "allow_audit_log:view"]
}
```

Fix it by creating a key that includes the `requiredPermission` — permissions
cannot be edited on an existing key.

<Note>
  `403 insufficient_scope` comes from the coarse scope layer. You will see it
  most often on legacy keys created before explicit permissions shipped, but any
  key whose (derived) scopes do not cover a scope-gated route — the evaluation
  endpoints, for example — receives it too. Permission denials on
  permission-gated routes return `insufficient_permission` instead.
</Note>
