---
title: "List API keys"
method: GET
path: "/v1/api-keys"
tags: ["API keys"]
---

# List API keys

`GET /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
  - OAuth 2 (authorization code, authorization URL `https://api.kawaa.com/oauth/authorize`, token URL `https://api.kawaa.com/oauth/token`, scopes: `security:read`)

Every active key on the account, with the scope vocabulary beside them so a caller that can list keys can also see what a narrower one could be limited to. Never returns a secret: `key_preview` is the stored masked form, and a key created before previews were stored shows `ev_••••`.

## Response `200`

The keys, and the scope vocabulary.

- object
  - `success` unknown, required
  - `data` object, required
    - `api_keys` ApiKeySummary[], required
      - `key_id` string, required
      - `name` string, required
      - `status` string, required
      - `created_at` string, date-time
      - `last_used_at` string, date-time, nullable
      - `key_preview` string: `ev_****` and the key's last four characters, never the secret. An older key that has no stored preview shows `ev_••••`, with no characters from the key.
      - `scopes` string[], nullable, required: What this key may reach. **`null` means UNRESTRICTED**: every key created without a `scopes` array is, and that is the default. Always present, so `null` is a fact about the key rather than a field an older build omitted.
    - `available_scopes` object[], required: The whole scope vocabulary, in the order a UI should present it.
      - `scope` 'verification:read' | 'verification:write' | 'lists:read' | 'lists:write' | 'deliverability:read' | 'deliverability:write' | 'compose:read' | 'compose:write' | 'exports:read' | 'exports:write' | 'webhooks:read' | 'webhooks:write' | 'integrations:read' | 'integrations:write' | 'analytics:read' | 'analytics:write' | 'account:read' | 'account:write' | 'account:delete' | 'billing:read' | 'billing:write' | 'team:read' | 'team:write' | 'security:read' | 'security:write' | 'white_label:read' | 'white_label:write', required
      - `description` string, required
      - `sensitive` boolean, required: Marks a scope that spends money, destroys data or changes who has access. Present these differently from a read scope.
    - `recommended_agent_scopes` string[], required: What to give an AI agent doing list hygiene: verify, watch the job, read the result, see what it cost. Nothing that spends money outside verification credits, nothing that changes access, nothing destructive. Published here so the safe default is the discoverable one.

## Other responses

- `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.
- `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)
