> ## 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.

# Authentication

> Authenticate Cactal REST requests with API keys and MCP requests with OAuth or API keys.

REST requests to `api.cactal.ai` authenticate with an API key sent as a bearer token:

```bash theme={null}
curl https://api.cactal.ai/v1/websites \
  -H "Authorization: Bearer $CACTAL_API_KEY"
```

There is no cookie or session authentication on the public API host. A request that carries a browser session cookie is rejected with `401` so that keys and sessions can never be confused.

The MCP endpoint also supports OAuth authorization code flow with PKCE. Standard MCP clients can connect to `https://api.cactal.ai/v1/mcp` without a manually configured secret, sign in to Cactal, and approve access. The resulting token is valid only for the MCP resource and uses the signed-in user's grants. See [Connect over MCP](/docs/agents/mcp).

## Keys

A key's secret is a 43-character base64url string generated from 32 random bytes. Cactal stores only its SHA-256 hash:

* The plaintext is returned **once**, when the key is created or rotated.
* Nothing on Cactal's side can reveal it again — treat loss as a rotation event.
* Expired (`expiresAt` in the past) and revoked keys fail authentication with `401`.

Create keys in the [dashboard](https://app.cactal.ai) or with `POST /v1/apiKeys` — see [Create an API key](/docs/create-an-api-key).

## Access scopes

Each key carries an `access` object that fixes what it can reach:

```json Organization-scoped access theme={null}
{ "kind": "organization", "role": "admin" }
```

```json Website-scoped access theme={null}
{
  "kind": "websites",
  "websiteIds": ["V1StGXR8Z5jdHi6BmyTxQ"],
  "role": "content_editor"
}
```

| Scope kind     | Reaches                                                              | Roles                                    |
| -------------- | -------------------------------------------------------------------- | ---------------------------------------- |
| `organization` | Every website in the organization, plus organization-level resources | `admin`, `full_editor`, `content_editor` |
| `websites`     | Only the listed websites                                             | `full_editor`, `content_editor`          |

The `owner` role is not grantable to keys. Update a key's scope at any time by sending `access` to `PATCH /v1/apiKeys/{apiKeyId}` — the change applies immediately.

## Roles and capabilities

Roles expand to capabilities — the permission vocabulary that gates every endpoint. Each endpoint in this reference declares its requirement as `x-cactal-required-capability` in the OpenAPI spec (for example, `websites.create` or `cms.items.setPublished`).

| Role             | Covers                                                                                  |
| ---------------- | --------------------------------------------------------------------------------------- |
| `content_editor` | CMS content, assets, analytics reads — no source code, publishing, or settings          |
| `full_editor`    | Everything above, plus source code, publishing, domains                                 |
| `admin`          | Everything above, plus members, websites, API keys, audit log (organization scope only) |

The [MCP server](/docs/agents/mcp) uses the same model: `tools/list` only shows tools the presented user or key could actually call.

## Authentication vs authorization errors

| Situation                                                                      | Response             |
| ------------------------------------------------------------------------------ | -------------------- |
| No key, malformed header, expired, or revoked key                              | `401` `unauthorized` |
| Valid key, but the operation needs a capability the key lacks                  | `403` `forbidden`    |
| Valid key, but a website is outside the key's scope or in another organization | `404` `not_found`    |
| Valid key, but you lack a grant on an organization that exists                 | `403` `forbidden`    |

For website resources, no-access returns `404` rather than `403`, so website existence never leaks. Organization-scoped endpoints instead return `403` when you lack a grant on an existing organization, and `404` only when the organization id is unknown.

## Key lifecycle

| Action                       | Endpoint                             | Effect                                                                    |
| ---------------------------- | ------------------------------------ | ------------------------------------------------------------------------- |
| Create                       | `POST /v1/apiKeys`                   | Returns the record plus `plaintextKey` (only time)                        |
| Rotate                       | `POST /v1/apiKeys/{apiKeyId}/rotate` | New secret, same identity and scope; old secret stops working immediately |
| Revoke                       | `POST /v1/apiKeys/{apiKeyId}/revoke` | Permanent; the key cannot be reactivated                                  |
| Rescope / rename / re-expire | `PATCH /v1/apiKeys/{apiKeyId}`       | Update `name` or `expiresAt`; sending `access` replaces the access object |

<Warning>
  Rotation and revocation take effect on the next request. In-flight abuse stops as soon as you rotate. Make `rotate` your first response to a suspected leak, then review the key's recorded actions via `GET /v1/organizations/{organizationId}/audit-log`, list its pending content changes with `GET /v1/websiteSourceCode/changes`, and inspect selected files with `GET /v1/websiteSourceCode/files/diff`.
</Warning>

## Storage rules

* Keep keys in a secret manager or environment variables; never in code, logs, or client-side bundles.
* Issue one key per integration or agent so you can rotate and audit them independently.
* Prefer website-scoped `content_editor` keys for agents that only manage content — see [Agent-safe publishing](/docs/agents/agent-safe-publishing).
