Skip to main content
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:
{
  "meta": {
    "requestId": "req_abc123xyz789"
  },
  "data": {}
}
Paginated responses keep the result collection in data and include pagination metadata in a top-level pagination object:
{
  "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 logs.

Handling pagination

  1. Make your initial request.
  2. Check pagination.hasMore.
  3. Use pagination.cursor for the next request.
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:
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.