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

# API design overview

> Design philosophy for Unkey APIs

Unkey APIs prioritize developer experience, consistency, and clarity. This document outlines core design decisions and how to work with the API.

## Core principles

* Clear communication: structured responses make success and failure equally informative
* Practical over purist: pragmatic choices over rigid adherence to a single paradigm
* Predictable patterns: consistent endpoint behavior

## Response structure

All responses share a consistent envelope:

```json theme={"theme":"kanagawa-wave"}
{
  "meta": {
    "requestId": "req_abc123xyz789"
  },
  "data": {}
}
```

Paginated responses keep the result collection in `data` and include pagination
metadata in a top-level `pagination` object:

```json theme={"theme":"kanagawa-wave"}
{
  "meta": {
    "requestId": "req_abc123xyz789"
  },
  "data": [],
  "pagination": {
    "cursor": "cursor_xyz123",
    "hasMore": true
  }
}
```

List endpoints must use this shape unless the endpoint returns multiple
independent collections. Requests use `limit` and `cursor` fields in the JSON
body. Responses require `pagination.hasMore`. Include `pagination.cursor` only
when another page exists.

## Working with the API

### Always use the request ID

Every response includes a unique `requestId`. Include it when debugging or requesting support. You can also search for the request ID in <a href="https://app.unkey.com/logs" target="_blank">logs</a>.

### Handling pagination

1. Make your initial request.
2. Check `pagination.hasMore`.
3. Use `pagination.cursor` for the next request.

```js theme={"theme":"kanagawa-wave"}
const response = await fetch("https://api.unkey.com/v2/keys.listKeys", {
  method: "POST",
  headers: { Authorization: `Bearer ${rootKey}` },
  body: JSON.stringify({ apiId: "api_123" })
});

if (response.pagination?.hasMore) {
  await fetch("https://api.unkey.com/v2/keys.listKeys", {
    method: "POST",
    headers: { Authorization: `Bearer ${rootKey}` },
    body: JSON.stringify({
      apiId: "api_123",
      cursor: response.pagination.cursor
    })
  });
}
```

## Versioning

APIs use a major version in the URL, for example `/v2/`. Breaking changes increment the major version.

## OpenAPI examples

Every public v2 operation must include realistic OpenAPI examples. Examples are
part of the API contract because docs, SDKs, and agents use them to learn the
correct request and response shape.

Each operation needs:

* At least one request example
* At least one successful response example
* At least one example per status code or different outcome

Use Unkey-shaped values such as `api_1234abcd`, `key_1234abcd`,
`perm_1234abcd`, and `req_1234abcd`. Don't use placeholder names like `foo`,
`bar`, or `example` when a domain-specific value is available.

## Schema strictness

OpenAPI schemas are closed by default. Request bodies must use
`additionalProperties: false` unless the object is intentionally map-like.
Response objects must also be closed when the shape is known.

Use open objects only for explicit key/value maps, such as metadata fields,
analytics query rows, or user-provided attribute bags. Open objects must explain
what keys and values are valid. When possible, document size limits, value type
limits, and performance impact.

Don't use open objects as an escape hatch for incomplete modeling. If an API
field has known variants, model them explicitly in the schema.

## Idempotency and retries

Every operation must declare its idempotency and retry behavior. Retries are a
client-visible contract, especially for SDKs and agents that may retry requests
after connection errors or `5xx` responses.

Classify each operation as one of:

* `idempotent`: retrying the same request body produces the same final state
* `conditionally-idempotent`: retrying is safe only with a stable client value,
  such as an idempotency key or caller-provided resource identifier
* `not-idempotent`: retrying may create additional side effects

Be conservative. If the behavior isn't guaranteed by the implementation, mark
the operation as `not-idempotent` until the API provides a stronger contract.

OpenAPI operations must expose this as machine-readable metadata:

```yaml theme={"theme":"kanagawa-wave"}
x-unkey-idempotency: idempotent
```

Non-idempotent create, reroll, migration, and deployment operations must not be
auto-retried by generated SDKs unless they support an explicit idempotency
mechanism.

## Update semantics

Update endpoints use three-state field semantics:

* Omitted field: leave the existing value unchanged
* Field with a value: set or replace the value
* Field with `null`: clear the value, only when the schema explicitly permits
  `null`

Empty arrays and empty objects are values. They don't mean "omitted." For
example, `metadata: {}` replaces metadata with an empty object, and
`ratelimits: []` replaces the rate limit collection with an empty collection
when the endpoint defines full replacement behavior.

Document nullable clear behavior on each field that supports it. Don't rely on a
general update endpoint to imply that every field accepts `null`.

Replacement arrays replace the entire collection unless the endpoint is
explicitly named as an incremental action, such as `addPermissions` or
`removeRoles`.

## Resource identifiers

Prefer user-meaningful identifiers for public API inputs when they are stable and
unambiguous. Slugs, names, and external IDs are easier for humans and agents to
use than internal Unkey IDs because callers often know them without making an
extra lookup request.

Internal Unkey IDs are opaque. Don't require callers to parse their prefixes or
derive meaning from their structure. When an endpoint requires an internal ID,
document that the value must be fetched from another API response and include a
realistic example.

Every identifier field must document:

* Whether it accepts a slug, external ID, internal Unkey ID, or multiple forms
* Whether the value is caller-defined or generated by Unkey
* Whether the value is stable over the resource lifetime
* A realistic example value

Use internal IDs when slugs are mutable, ambiguous, unavailable, or unsafe for
the operation. Otherwise, prefer the identifier that users already understand.

## Delete behavior

Resource delete endpoints must be safe by default. Public API callers don't get
dashboard confirmation flows, and agents can delete resources accidentally.

For resources, prefer soft deletion first. Soft deletion immediately removes the
resource from normal use, then schedules hard deletion after 48 hours with a
delayed workflow. During the 48-hour window, users can restore the resource and
cancel the hard deletion.

Use one generic restore endpoint instead of one restore endpoint per resource
type. The caller passes the resource type and resource identifier in the request
body. The restore operation must validate that the resource type supports
restoration and that the resource is still inside its restore window.

Delete endpoints must document:

* Whether deletion is soft, hard, or configurable
* How long the restore window lasts
* How to restore the resource with the generic restore endpoint
* What related resources are affected immediately
* When hard deletion happens
* Whether repeated delete requests are idempotent
* Whether deleted resources remain visible in audit logs

Use immediate hard deletion only when retention is unsafe, illegal, or explicitly
requested by the caller through a clearly named option.

## Bulk operations

Bulk operations are atomic by default. They must either apply every requested
change or apply none of them. This matters for resources such as environment
variables, where partial success can leave deployments or runtime configuration
in an invalid state.

If an endpoint needs partial success behavior, the endpoint must document that
exception explicitly and return per-item results with enough detail to recover.

## Filtering and sorting

List endpoints use explicit, typed request body fields for filters. Don't add a
generic filter language unless the endpoint is inherently query-based, such as an
analytics endpoint.

When sorting is supported, expose sort fields as explicit enum values. Document
the default order and make it deterministic so pagination is stable.
