---
title: "Create an API key"
method: POST
path: "/keys"
tags: ["Keys"]
---

# Create an API key

`POST /keys`

## Request

- Base URL: `https://statable.com/api/v1`
- URL: `https://statable.com/api/v1/keys`
- Auth: HTTP bearer
- TypeScript: <https://skmtc.dev/statable/apis/statable-stats-api/docs/keys/post.ts> (a function with typed inputs; its response is checked against the schema)

Mints a key and returns its raw token EXACTLY ONCE. The requested scopes must be a subset of the authenticating key's scopes (else 403 `scope_escalation`), and a single-site key may only create keys locked to its own site (else 403 `key_not_scoped`). `expires_in_days` defaults to 90 and is capped at 365 — unlike the dashboard, the API cannot mint a key that never expires. Max 10 active keys per account.

## Request body

- CreateAPIKeyRequest
  - `name` string, required
  - `scopes` string[]: Defaults to ["read"]. Must be a subset of the authenticating key's scopes, else 403 scope_escalation.
  - `website_id` integer, nullable: Lock the new key to one site. Omit/null = all of the owner's sites. A single-site key MUST pass its own site id here.
  - `expires_in_days` integer: Outside 1–365 → 400 invalid_expiry.

## Response `200`

The created key, including the one-time raw token.

- CreatedAPIKey: The non-secret view of a key. The raw token is never stored and never appears here — only in the create/rotate responses.
  - `id` integer, required
  - `name` string, required
  - `prefix` string, required: Non-secret fragment of the token, to identify the key in a list.
  - `website_id` integer, nullable, required: The site the key is locked to; null = all of the owner's sites.
  - `scopes` string, required: Comma-separated scope set, in catalog order.
  - `created_at` string, date-time, required
  - `created_ip` string, nullable
  - `last_used_at` string, date-time, nullable
  - `last_used_ip` string, nullable
  - `expires_at` string, date-time, nullable: null = never expires. Only dashboard-created keys can be non-expiring; keys created over the API always carry an expiry.
  - `token` string, required: The raw bearer token. Store it now.

## Other responses

- `400`: Invalid request. Branch on the stable `code`: invalid_request (malformed body or a name outside 1–100 characters), invalid_scope (scope not in the vocabulary), invalid_expiry (expires_in_days outside 1–365), key_limit_reached (10 active keys already).
- `401`: Missing/malformed/invalid/revoked bearer token (`unauthorized`), or a key past its expiry (`key_expired`; the message names the expiry time).
- `403`: `insufficient_scope` (the key lacks `keys:manage`), `scope_escalation` (the requested scopes, or the target key's on rotate and revoke, exceed the authenticating key's scopes) or `key_not_scoped` (a single-site key tried to widen its site scope, or to rotate or revoke a key outside it).
- `404`: `write_disabled` (the write surface is off) or `unknown_site` (`website_id` does not exist or the owner cannot reach it — deliberately indistinguishable).
- `429`: Hourly account (default 2000/h) or per-key (default 600/h) limit hit (`rate_limited`). Retry after the `Retry-After` seconds. The X-RateLimit-* headers report the window that tripped (account on an account limit).
- `500`: Unexpected server error (`internal`). Report it with the `request_id` from the body or the X-Request-ID header: it is what lets the exact log entry be found, and without it a 500 can only be matched by guessing at a time window.

---

[API](https://skmtc.dev/statable/apis/statable-stats-api.md) - [All operations](https://skmtc.dev/statable/apis/statable-stats-api/llms.txt) - [OpenAPI document](https://skmtc.dev/statable/apis/statable-stats-api/revisions/352b6d47c186?raw)
