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

# API keys

> Workspace keys for CI jobs, scripts and backends: roles, project scope, expiry, IP allowlists, rate limits, idempotent retries and API versions.

An **API key** (`hk_…`) lets a system with no browser call the same admin API the console uses, and the hosted [MCP server](/connected-agents) at `/mcp`: a CI job, an infrastructure-as-code tool, a nightly script or your own backend. For a coding agent working with you interactively, [connect it over OAuth](/connected-agents) instead.

A key is not a user of its own. It **acts as the member who created it**, and its role is a ceiling on that member's role. So a key can never do more than its creator can do in the console, and it follows its creator: demote the creator and the key is demoted with them; remove them from the workspace and the key stops working.

## Create a key

<Steps>
  <Step title="Open Settings → API keys">
    In the console, open **Settings → API keys**. Owners and admins manage keys; other members see who to ask.
  </Step>

  <Step title="Create it">
    Choose **Create API key** and fill in:

    * **Name**: the system that will use it, such as `GitHub Actions deploy`, so you know what to revoke later.
    * **Role**: viewer, developer or admin. See [Roles](#roles).
    * **Projects**: **All projects**, or one project. See [Project scope](#project-scope).
    * **Expires**: in 90 days (the default), 30 days, 7 days, 1 year, or never.
    * **Allowed source IPs** (optional): the addresses the key may be used from.
  </Step>

  <Step title="Copy it now">
    The key is shown **once**. Hookie stores only a SHA-256 hash of the whole key and looks it up by that hash; the list shows the first 11 characters (`hk_` and 8 more) from then on. Store it in your CI system's secret store.
  </Step>
</Steps>

Only a person signed in to the console can create a key. A connected agent, or another API key, is refused, so one leaked or over-granted credential cannot mint more. You cannot create a key with a higher role than your own. While the workspace is suspended, creating a key is refused but revoking one still works.

Creating and revoking a key are recorded in **Settings → Audit log** (`create_api_key`, `revoke_api_key`). Every change a key makes is recorded as its creator, with the key named as the agent (`api_key:<id>`).

## Roles

| Role | The key can… |
| - | - |
| **Viewer** | List and read resources, records and deliveries. |
| **Developer** | Also create, change and delete project resources. |
| **Admin** | Also manage members, SSO, network access and the audit log. |

There is no owner role for a key, so **billing is never reachable with one**. Like a connected agent, a key also cannot reveal or rotate a destination's signing secret, manage connected agents, or create API keys. Those stay with a person in the console.

The role is applied on every request against the creator's role at that moment. A developer key made by an admin who is later made a viewer can only read.

## Project scope

A key for **one project** reaches:

* `/admin/api/projects/{its project id}/…`
* `/admin/api/me`
* the destination and source presets

Everything else answers `403` and names the routes the key can use. That includes the project list (it would name the other projects), other projects, the Default-project shortcuts such as `/admin/api/rules`, and workspace-wide routes such as members, SSO and the audit log.

A key for **all projects** reaches whatever its role allows across the workspace.

## Expiry and IP allowlist

An expired key is refused with `401`, exactly like a revoked one. Over the API, `expires_at` is an ISO 8601 time in the future. Leave it out, or send `null`, for a key that never expires.

A key's **allowed source IPs** hold up to 64 IPv4 or IPv6 addresses or CIDR ranges. A request from any other address is refused with `403`; on `/admin/api` the refusal is recorded in the audit log as `access_denied`. A key with an allowlist is refused when the request's source address cannot be determined. An empty list accepts any address.

The list shows each key's role, project, expiry, allowed IPs, creator, when it was created and when it was last used, with a status of **active**, **expired** or **revoked**. Last used is updated at most once a minute.

## Use a key

Send it as a bearer token:

```bash theme={"dark"}
curl "https://app.hookie.ai/admin/api/projects" \
  -H "Authorization: Bearer $HOOKIE_API_KEY"
```

```bash theme={"dark"}
curl -X POST "https://app.hookie.ai/admin/api/projects/$PROJECT_ID/destinations" \
  -H "Authorization: Bearer $HOOKIE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"Warehouse","url":"https://example.com/hook"}'
```

A write made with a bearer token needs no CSRF header; that check applies only to the console's session cookie. `GET /admin/api/me` names the key the request was made with under `api_key` (`id`, and `project_id` for a key scoped to one project).

The hosted MCP server accepts a key the same way, so an MCP client that cannot run the OAuth flow can send one as a header. The key's role decides which tools it can call: a viewer key reaches the `hookie:read` tools, a developer key also the `hookie:write` ones, and an admin key the `hookie:manage` ones too.

<CodeGroup>
  ```bash Claude Code theme={"dark"}
  claude mcp add --transport http hookie https://app.hookie.ai/mcp \
    --header "Authorization: Bearer $HOOKIE_API_KEY"
  ```

  ```json .mcp.json theme={"dark"}
  {
    "mcpServers": {
      "hookie": {
        "type": "http",
        "url": "https://app.hookie.ai/mcp",
        "headers": { "Authorization": "Bearer ${HOOKIE_API_KEY}" }
      }
    }
  }
  ```
</CodeGroup>

<Warning>
  Never commit a key. Reference it from an environment variable, as above, and keep the value in your secret store.
</Warning>

## Rate limits

Each credential may make **100 requests every 10 seconds** to `/admin/api` and `/mcp` combined. Each API key, each connected agent and each person signed in to the console is counted separately, so one runaway script cannot lock out the rest of your team.

Over the limit, the request is refused with `429` and a `Retry-After: 10` header. On `/admin/api` the body is:

```json theme={"dark"}
{ "error": "Too many requests — retry in 10 seconds", "retry_after": 10 }
```

On `/mcp` the same `429` and `Retry-After` carry a JSON-RPC error (code `-32029`). Wait the number of seconds in `Retry-After`, then retry.

## Idempotent retries

Send an `Idempotency-Key` header on a `POST` to `/admin/api`, and a retry of that request is answered once, not run twice. Use a new unique value, such as a UUID, for each distinct request, and the same value when you retry it.

* The first request runs, and its response is stored, encrypted, for **24 hours**.
* A retry with the same key, from the same credential, to the same path with the same body gets the stored response back with `Idempotent-Replayed: true`. Nothing runs again.
* The same key with a **different body, path or credential** is refused with `422`. Use a new key for a new request.
* A retry that arrives **while the first request is still running** gets `409` with `Retry-After: 1`. A request still unfinished after 60 seconds is treated as abandoned, and its key is free again.
* A **server error (5xx)** is not stored, so you can retry with the same key. An error in the `4xx` range is stored and replayed like a success.

The key is 1 to 255 printable characters with no spaces; anything else is `400`. It is ignored on methods other than `POST`. A response larger than 64 KiB, or one that is not JSON, is not stored. A workspace can hold 10,000 keys in any 24 hours; past that, a request with a new key is refused with `429` and `Retry-After: 3600`.

`POST /admin/api/api-keys` refuses the header with `400`: its response is a key that is shown once, and a stored copy would be a second way to recover it.

## Versioning

Every response from `/admin/api` and `/mcp` carries the API version, errors and `429`s included:

```http theme={"dark"}
Hookie-Version: 2026-09-29
```

Additive changes (a new route, a new response field, a new optional parameter) ship under the current version, so ignore fields you do not recognise. A breaking change gets a new dated version, and the previous version stays available for at least 12 months.

To pin a version, send it as a `Hookie-Version` request header. A version this deployment does not serve is refused with `400`, and the body lists `supported_versions`. Today that is `2026-09-29` only.

## Revoke a key

Choose **Revoke** on the key's row and confirm. Its next request is refused with `401`. Keys cannot be edited or rotated: to change a key's role, project, expiry or allowlist, create a new one, move your system over, then revoke the old one.

## API

Owners and admins, or an admin-role credential:

```http theme={"dark"}
GET    /admin/api/api-keys          # list keys: never the key itself
POST   /admin/api/api-keys          # create: {name, role, project_id?, expires_at?, ip_allowlist?}
DELETE /admin/api/api-keys/{id}     # revoke
```

`role` is `viewer`, `developer` or `admin`. `project_id` is a project's id, or `null` for every project. The `201` response carries the key once, in `key`, with `Cache-Control: no-store`. Only a person signed in to the console can create a key; see [Create a key](#create-a-key). The full contract is in the [API reference](/api-reference).
