Backend API, API keys, introspection

Server-to-server surface for first-party relying parties. The REST part is a subset of the Clerk Backend API (2026-08-20); token checks use the OAuth standards. Anything without a Clerk or Auth0 equivalent lives in GraphQL.

Enabling a client

Two GraphQL mutations on manage { client(clientId: ...) { ... } }, both on the client settings page:

setAllOrganizations(enabled: Boolean!)

Admin only ("All organizations (first-party)" section). Eligible organizations of a user are all of their memberships. No per-organization client access (oauth_client_organizations), no organization consent, no access-management screen; prompt=consent does not show it either. Covers the authorize flow, the orgs claim, refresh re-checks and the account pages. Use organization_usage=none plus the organizations scope for a silent orgs list.

setBackendApi(enabled: Boolean!)

Anyone who can manage the client (owner, or org:clients for organization clients; "Backend API" section). Enables the REST API below for this client's secret, which then acts for every user who authorized the client. Requires a confidential client with a client secret and subject_type=public, subject_scope=global (so sub is the U... user id). Checked when enabled and on every request.

Backend REST API

Base <root>/api/v1, so a Clerk SDK works with apiUrl = <root>/api. Auth: Authorization: Bearer <client secret>. Pagination limit/offset → {"data": [...], "total_count": n}. Timestamps are unix milliseconds. Errors: {"errors": [{"message", "long_message", "code"}]} with authentication_invalid (401), authorization_invalid (403), resource_not_found (404), bad_request (400), form_param_format_invalid / form_param_missing / form_identifier_exists (422), too_many_keys (409).

Visibility

Invisible users, organizations and keys are 404.

Regular client allOrganizations client

Users

consented to the client (any identity or audience scope)

consented, or a member of any organization

Organizations (/organizations, org key subjects)

enabled the client (oauth_client_organizations)

all

Memberships of a user

eligible and granted to the client by that user (normal organization access)

all memberships

POST /organizations

created_by required and visible; the new organization enables the client and records the creator's grant

created_by optional

Organization API keys

organization visible; created_by visible, holds org:api-keys and granted the organization to the client

same, every membership counts as granted

Endpoint Notes

GET /users/{user_id}

Clerk User subset

GET /users/{user_id}/organization_memberships

Memberships granted to this client

GET /organizations

limit, offset, query (name or slug)

GET /organizations/{organization_id}

id or slug

POST /organizations

name, slug?, created_by (see visibility), public_metadata?, private_metadata?; created_by gets role:org-admin; slug derived from the name when omitted

POST /api_keys

type?, name, description?, subject, scopes?, claims?, created_by?, seconds_until_expiration?; returns secret once

GET /api_keys

subject (required), type?, include_invalid?, limit, offset, query

GET, PATCH, DELETE /api_keys/{id}

PATCH: description, claims, scopes, seconds_until_expiration

POST /api_keys/{id}/revoke

revocation_reason?; soft revoke

POST /api_keys/verify

secret; 404 unless active and issued to this client

Organization API keys need created_by with the org:api-keys organization permission (granted to role:org-admin). User API keys may only be created by their subject (or without created_by).

Differences from Clerk

  • Ids are pykrete ids: users U..., organizations O..., API keys K... (letter + 32 alphanumerics), not user_ / org_ / ak_. Clerk's OpenAPI patterns reject them; the SDKs do not check.

  • API key secrets are DELTA-API-KEY-<48 alphanumerics> and only their SHA-256 is stored, so GET /api_keys/{id}/secret is not offered.

  • A member can hold several pykrete roles: role is the primary one (role:org-admin, else role:org-member, else the first), permissions is the union of org:* permissions of all of them.

  • User lacks phone numbers, wallets, external accounts and sign-in timestamps (empty or null). Email ids (E...) and membership ids (M...) are derived hashes. Organization.updated_at equals created_at.

  • Keys are bound to the client that created them; other clients see them as nonexistent.

Introspection and revocation

POST /oauth/introspect (RFC 7662) and POST /oauth/revoke (RFC 7009), advertised in discovery. Client authentication: HTTP Basic (form-urlencoded id and secret, RFC 6749 2.3.1) or client_id/client_secret in the body; the client must be confidential. Body: token, token_type_hint?, form or JSON.

Only tokens issued to the calling client are reported; everything else is exactly {"active": false}.

API key:

{"active": true, "kind": "api_key", "token_type": "Bearer", "iss": "<issuer>",
 "client_id": "C...", "api_key_id": "K...", "name": "...", "scope": "a b",
 "sub": "U..." | "org_id": "O...", "created_by": "U...", "claims": {...},
 "iat": 1700000000, "exp": 1700003600}

Access token: kind: "access_token", iss, client_id, sub, scope, aud (string for one audience, else array), iat, exp, org_id and org_roles when an organization was active. Times are unix seconds.

Revocation always answers 200: API keys are soft-revoked, access tokens deleted (with their refresh token), refresh tokens revoke their chain.