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

# Manage API keys

> Learn how to scope, create, rotate, and revoke Cactal API keys: organization-wide versus website-scoped access, expiry, the one-time plaintext rule, per-website key audits, and a rotation runbook.

API keys are the permission boundary for every integration and agent, so scope them tightly and rotate them on a schedule.

## Prerequisites

* An API key with `admin` organization access, or the dashboard — see [Create an API key](/docs/create-an-api-key)
* A website id, if you are creating website-scoped keys — see the [Quickstart](/docs/quickstart)

## Scoping strategy

A key's `access` takes one of two shapes:

| Shape             | Fields                                                                                     | Grants                                             |
| ----------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------- |
| Organization-wide | `{ "kind": "organization", "role": "admin" \| "full_editor" \| "content_editor" }`         | The role across every website in the organization. |
| Website-scoped    | `{ "kind": "websites", "websiteIds": ["..."], "role": "full_editor" \| "content_editor" }` | The role on the listed websites only.              |

The `owner` role is never grantable to a key. Apply least privilege, especially for agents:

* A content agent for one client site: `websites` scope with `content_editor`.
* A site-building agent: `websites` scope with `full_editor` on that website.
* Fleet automation (creating websites, managing members and keys): `organization` scope with `admin`.

One key per agent per concern. Blast radius on a leak stays one website, one role.

<Steps>
  <Step title="Create a scoped key">
    `POST /v1/apiKeys` requires `expiresAt` — pass an ISO datetime, or `null` for no expiry.

    ```bash Create a website-scoped key theme={null}
    curl -X POST 'https://api.cactal.ai/v1/apiKeys' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{
        "organizationId": "oG8hJ0kL2mN4pQ6rS7tU9",
        "name": "acme-content-agent",
        "expiresAt": "2026-10-01T00:00:00Z",
        "access": { "kind": "websites", "websiteIds": ["V1StGXR8_Z5jdHi6B-myT"], "role": "content_editor" }
      }'
    ```

    ```json Response theme={null}
    {
      "apiKey": {
        "id": "kA1bC3dE5fG7hI9jK0lM2",
        "organizationId": "oG8hJ0kL2mN4pQ6rS7tU9",
        "name": "acme-content-agent",
        "expiresAt": "2026-10-01T00:00:00.000Z",
        "revokedAt": null,
        "createdAt": "2026-07-08T15:04:05.000Z",
        "access": { "kind": "websites", "websiteIds": ["V1StGXR8_Z5jdHi6B-myT"], "role": "content_editor" }
      },
      "plaintextKey": "exampleApiKeySecret000000000000000000000000"
    }
    ```

    <Warning>
      `plaintextKey` appears exactly once, here and on rotate. Cactal stores only a SHA-256 hash. Store it in a secret manager immediately; if you lose it, rotate.
    </Warning>

    Expired and revoked keys fail authentication with `401`.
  </Step>

  <Step title="Update name and expiry">
    `PATCH /v1/apiKeys/{apiKeyId}` changes metadata without touching the secret:

    ```bash Shorten the expiry theme={null}
    curl -X PATCH 'https://api.cactal.ai/v1/apiKeys/kA1bC3dE5fG7hI9jK0lM2' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "name": "acme-content-agent (sunset)", "expiresAt": "2026-08-01T00:00:00Z" }'
    ```

    You can shorten or extend expiry on an active key. Once a key is expired or revoked it is inactive for good — setting a future `expiresAt` returns `409`.
  </Step>

  <Step title="Change a key's access">
    Sending `access` to `PATCH /v1/apiKeys/{apiKeyId}` replaces the key's grants atomically — there is no partial merge. Widen a client key to a second website, or tighten an organization key down to specific websites:

    ```bash Rescope to two websites theme={null}
    curl -X PATCH 'https://api.cactal.ai/v1/apiKeys/kA1bC3dE5fG7hI9jK0lM2' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "access": { "kind": "websites", "websiteIds": ["V1StGXR8_Z5jdHi6B-myT", "W2tUYr9_a6keIj7C-nzV"], "role": "content_editor" } }'
    ```

    Every `websiteId` must belong to the key's organization, or the call fails with `400`. The secret is unchanged, so running integrations keep working under the new scope.
  </Step>

  <Step title="Rotate the secret">
    `POST /v1/apiKeys/{apiKeyId}/rotate` issues a new secret for the same key id and access. The old secret stops authenticating the moment the call returns.

    ```bash Rotate theme={null}
    curl -X POST 'https://api.cactal.ai/v1/apiKeys/kA1bC3dE5fG7hI9jK0lM2/rotate' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    The response repeats the `apiKey` metadata with a fresh one-time `plaintextKey`. Inactive keys cannot rotate (`409`).
  </Step>

  <Step title="Revoke a key">
    `POST /v1/apiKeys/{apiKeyId}/revoke` disables the key permanently and idempotently:

    ```bash Revoke theme={null}
    curl -X POST 'https://api.cactal.ai/v1/apiKeys/kA1bC3dE5fG7hI9jK0lM2/revoke' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    The response is the key's metadata with `revokedAt` set. Revocation is the correct end state for offboarded agents — revoked keys stay listed for audit history.
  </Step>

  <Step title="Audit keys per website">
    `GET /v1/apiKeys?organizationId=...` lists every key in the organization with its access. To answer "which keys can touch this client's website", pass `websiteId` instead:

    ```bash List keys scoped to a website theme={null}
    curl 'https://api.cactal.ai/v1/apiKeys?websiteId=V1StGXR8_Z5jdHi6B-myT' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    <Check>
      You should see `{ "items": [ { "id": "...", "name": "acme-content-agent", "access": { "kind": "websites", "websiteIds": ["V1StGXR8_Z5jdHi6B-myT"], "role": "content_editor" }, ... } ], "nextCursor": null }`. This lists keys with a direct grant on the website; organization-wide keys appear only in the organization listing. Pass exactly one of `organizationId` or `websiteId`.
    </Check>

    Key creation, updates, access changes, rotation, and revocation are all recorded in the organization's audit log.
  </Step>
</Steps>

## Key rotation runbook

For a suspected leak, rotate in place — the leaked secret dies instantly:

1. `POST /v1/apiKeys/{apiKeyId}/rotate` and capture the new `plaintextKey`.
2. Update the secret in your secret manager and redeploy consumers.
3. Expect `401`s from consumers between steps 1 and 2; they confirm the old secret is dead.

For routine, zero-downtime rotation, use create-then-revoke:

1. Create a new key with identical `access` and a dated name.
2. Roll consumers to the new secret and verify traffic with the old key has stopped.
3. Revoke the old key.

## Troubleshooting

| Symptom                                                         | Cause                                                                    | Fix                                                                          |
| --------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `401` unauthorized on API calls                                 | The key is revoked, expired, or the plaintext is wrong.                  | List keys to check `revokedAt`/`expiresAt`; rotate or create a replacement.  |
| `409` `Inactive API keys cannot be rotated`                     | Rotate targets a revoked or expired key.                                 | Create a new key instead.                                                    |
| `400` `One or more websites do not belong to this organization` | `websiteIds` includes a website from another organization.               | Scope the key inside one organization; transfer the website first if needed. |
| `403` on key endpoints                                          | Your calling key lacks `admin` organization access.                      | Manage keys with an `admin`-role key or from the dashboard.                  |
| Key works on one website but not another                        | The key is website-scoped and the second website is not in `websiteIds`. | Update the key's access, or use an organization-wide key.                    |

## Next steps

* [Authentication](/docs/api-reference/authentication) for the bearer scheme and key format
* [Agent-safe publishing](/docs/agents/agent-safe-publishing) — scoped keys as the agent permission boundary
* [Security](/docs/platform/security) for key hashing and tenant isolation
