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

# Create an API key

`POST /v1/api-keys`

## Request

- Base URL: `https://api.kawaa.com`
- URL: `https://api.kawaa.com/v1/api-keys`
- Auth: one of:
  - API key in header `X-Api-Key`
  - HTTP bearer

The secret is returned once and never again: `data.api_key.key` is the only place it ever appears.

Accepts `name` and, optionally, `scopes`; any other field is a 400 rather than silently ignored. A new key is refused with a 400 (`Maximum of 10 API keys allowed per account`) while the account already has 10 active keys, on every plan (dashboard sign-in sessions and connected apps do not count); revoke one first. The cap is checked only here: keys suspended when a subscription ended are not counted, and resubscribing restores them, so an account can hold more than 10 that way and then cannot create another until it is under 10. `GET /v1/usage/limits` publishes the cap as `limits.api_keys`. Omitting `scopes` creates an unrestricted key. Sending `scopes` creates a key limited to exactly those scopes: an empty array is refused (a key that can do nothing is never what was meant) and so is `null` (omitted and null are not the same thing, and this route will not guess which was meant). A scope a key does not hold answers `403 INSUFFICIENT_SCOPE` on use, naming what it needed.

Creating a key with a dashboard session needs a sign-in within the last 15 minutes; an older session answers `403 REAUTHENTICATION_REQUIRED`. Creating one with an API key has no such limit, but the key used must itself be unrestricted: no scope grants minting.

## Request body

- object
  - `name` string, required: A label for the key, for your own benefit. **Measured after trimming**: surrounding whitespace is trimmed before the 1-100 length check, so a 100-character name padded with spaces is accepted and stored trimmed, while a raw `maxLength` would refuse it, and `" "` satisfies a raw `minLength: 1` and is refused anyway. The `\S` pattern stays because it is the one part that survives trimming: it requires at least one non-whitespace character.
  - `scopes` string[]: Limit the new key to these scopes. Omit the field for an unrestricted key; `[]` and `null` are both 400s. A `:write` scope implies its `:read` half, so `verification:write` alone can read results. The stored list is normalized to this document's order, so the response may not list them as sent. Duplicates are accepted and collapsed into one de-duplicated list in a fixed order, so `["verification:read", "verification:read"]` creates the same key as one entry. No `uniqueItems` here for that reason: it would refuse locally a payload the API takes. The `maxItems` limit applies to the RAW array, before de-duplication.

## Response `201`

The new key. Save it now.

- object
  - `success` unknown, required
  - `data` object, required
    - `api_key` object, required: The only response that ever contains the secret.
      - `key` string, required: THE SECRET. Returned here and nowhere else; no later call can recover it. Store it before reading the rest of this object.
      - `key_id` string, required: The key's identifier, used to revoke it. Not secret.
      - `name` string, required
      - `created_at` string, date-time, required
      - `scopes` string[], nullable, required: The scopes the key was created with, normalized and ordered. `null` means unrestricted. Always present, so a client never has to tell 'unrestricted' from 'the field was omitted'.
      - `warning` string, required: The one-shot warning, as sent.

## Other responses

- `400`: The request is malformed, or names a field this endpoint does not accept. Retrying it unchanged will fail identically.
- `401`: No credential, or one that is not accepted. `code` distinguishes `API_KEY_SUSPENDED` (subscription ended; restartable) from `API_KEY_REVOKED`, `SESSION_EXPIRED` and `OAUTH_TOKEN_EXPIRED` (a connected app's one-hour access token lapsed: refresh it and retry, or sign in again if no refresh token was issued or the refresh is refused).
- `403`: Refused, and **the code says which of three problems it is**, and they need different remedies: - `INSUFFICIENT_SCOPE`: the credential is valid but not scoped for this route. With an API key, issue one that carries the scope named in the message. With an OAuth access token, connect the app again asking for that scope as well as the ones it has, unless the message says a connected app can never be given it; then use an API key with every scope the message names, or have the account owner do it in the dashboard. - `PLAN_UPGRADE_REQUIRED`: the account's plan does not include the feature. **No key of any scope reaches it**, so issuing another is wasted work; the remedy is a plan change. Returned by the verification routes for `options.enrich`, and by the finder, the monitors, the audit log, auto-clean and the white-label routes. - `PLAN_LIMIT_EXCEEDED`: the plan includes the feature and the account is at its ceiling (webhook endpoints, team seats, integrations, auto-clean lists). Delete one or upgrade. - `FORBIDDEN`: refused for a reason none of the above covers. `ErrorCode` is an open set, so branch on the code with a default rather than exhaustively.
- `429`: Too many requests. When the server knows how long to wait, `Retry-After` says so and `error.retry_after_seconds` carries the same number; the `X-RateLimit-*` headers say where you stand.
- `500`: The key could not be created and the outcome is **indeterminate**. An unexpected failure while saving the key answers this, so the save may have completed with the response lost. That matters more here than on most routes, because **the secret is returned once**. If the write landed, the key now exists on the account and nobody has its value: it cannot be recovered, only revoked. Do not retry blind: read `GET /v1/api-keys`, revoke anything that appeared without a secret you hold, and only then create another.
- `default`: Any status this operation does not list. Kawaa's own errors carry the error envelope. The gateway in front of the API can answer any operation with its own bare body (`GatewayError`): a `503` after about 29 seconds, when the request may still have run and been charged, a `413` for a body over about 6 MB, or a `400` for a path with malformed percent-encoding.

---

[API](https://skmtc.dev/kawaa/apis/kawaa-api.md) · [All operations](https://skmtc.dev/kawaa/apis/kawaa-api/llms.txt) · [OpenAPI document](https://skmtc.dev/kawaa/apis/kawaa-api/revisions/d27c9c33f6ba?raw)
