Skip to content

Management API

The Management API is the control plane behind the keylight CLI and MCP server — one API, many thin clients. It lets you (and agents acting for you) create apps, configure key types, mint licenses, connect payment providers, and verify your setup without opening the dashboard.

This is a different surface from the SDK API: the SDK endpoints (activate, validate, …) are called by your app with an SDK key; the Management API is called by you or your tooling with a personal access token.

https://api.keylight.dev/v1

Every endpoint is tenant-scoped by the token — there is no tenant id in the path. Requests and responses are JSON.

Send a personal access token as a bearer header:

Authorization: Bearer klm_...

Tokens are minted in the dashboard under Settings → API tokens, shown once, and carry a fixed set of scopes chosen at creation. A missing or bad token is 401; a token that lacks a required scope is 403.

For agents and CI, set KEYLIGHT_API_TOKEN in the environment — no browser needed.

The keylight CLI authorizes with the device-authorization flow, so it works the same everywhere — desktop, a server over SSH, or CI.

  1. Run keylight login. It calls POST /v1/login/device and prints a short code plus a URL.
  2. Open the URL in any browser, sign in, and confirm the access the CLI asked for (you can narrow it; destructive danger access is off unless you turn it on).
  3. The CLI polls POST /v1/login/device/token and stores the token once you approve.

For non-interactive use (agents, CI), set KEYLIGHT_API_TOKEN instead — no browser step. keylight logout (or POST /v1/logout) revokes the token.

ScopeGrants
products:read / products:writeRead / manage apps and key types
licenses:read / licenses:writeRead / mint, remint, deactivate, import licenses
integrations:read / integrations:writeRead / manage payment providers, webhook, SDK key
customers:read / customers:writeRead / manage customers and entitlements
usage:readRead usage statistics
dangerRequired in addition to the resource scope for every confirm-gated operation

Never ship an unscoped token. Give an agent only the scopes its task needs.

Destructive, hijack-shaped, or secret-revealing operations are not removed from the API — they are confirm-gated, in the style of npm’s browser login. The gated operations are: delete a key type, revoke a license, export licenses, read or rotate the SDK key, set the webhook URL, and rotate a provider secret.

A gated call with no confirmation does not execute. Instead:

  1. You call the endpoint normally. The server parks a pending confirmation and returns 202:

    {
    "confirmation": {
    "id": "9f3c…",
    "status": "pending",
    "approveUrl": "https://app.keylight.dev/dashboard/<tenant>/confirmations/9f3c…",
    "expiresInSeconds": 600
    }
    }
  2. A human opens approveUrl in a dashboard session, reads a plain-language description of exactly what will happen, and approves or denies.

  3. You poll GET /v1/confirmations/{id} until status is approved, denied, or expired. Honour the Retry-After header.

  4. On approved, re-issue the identical request with the confirmation id in a header:

    X-Keylight-Confirmation: 9f3c…

    The server verifies the request parameters are byte-identical to the ones that were approved, consumes the confirmation atomically, and executes exactly once — even under racing retries. A denied, expired, already-consumed, or parameter-mismatched confirmation is 403.

List endpoints are cursor-paginated with ?cursor= and ?limit= (max 100, default 25):

{ "items": [ /* … */ ], "nextCursor": "eyJ…" }

Follow nextCursor until it is null. Ordering is stable (newest first, with an id tiebreak).

POST /v1/licenses (mint) and POST /v1/licenses/import accept an Idempotency-Key request header. The first success is cached for 24 hours keyed on that value; a replay with the same key returns the stored response with Idempotent-Replay: true and does not re-execute. Agents that retry on timeout will never double-mint.

Every error is a single-field envelope:

{ "error": "message" }
StatusMeaning
400Malformed request (bad JSON or invalid parameters)
401Missing, expired, or revoked token
402The operation requires a higher Keylight plan
403Missing scope, or a denied / expired / mismatched confirmation
404Absent or cross-tenant resource (indistinguishable — no existence leak)
405Wrong method for the path
409State conflict (e.g. already revoked, key type still in use)
422Well-formed but semantically invalid
429Rate limited — retry after Retry-After

Raw license keys appear only once, in the mint and remint responses. The SDK key, the webhook signing secret, and the Gumroad ping URL are each revealed exactly once on their gated path. List and detail responses only ever carry masked display forms.

GroupEndpoints
ProductsGET/POST /products, GET/PATCH /products/{id}, POST/DELETE /products/{id}/test-mode, POST /products/{id}/free-tier
Key typesGET/POST /products/{id}/key-types, PATCH/DELETE /products/{id}/key-types/{ktId} (delete gated), POST …/verify-prices
LicensesGET/POST /licenses, GET /licenses/{id}, POST /licenses/{id}/revoke (gated), POST /licenses/{id}/remint, POST /licenses/{id}/deactivate-device, POST /licenses/import, GET /licenses/export (gated)
SDK keyGET /sdk-key (gated), POST /sdk-key/rotate (gated)
IntegrationsGET /integrations, POST /integrations/{provider}/connect, DELETE /integrations/{provider}, POST /integrations/{provider}/rotate-secret (gated), POST /integrations/stripe/connect, GET /integrations/stripe/status, POST /products/{id}/integrations/{provider}
WebhookGET /webhook, PUT /webhook (gated)
CustomersGET/POST /customers, GET /customers/{id}, POST /customers/{id}/link-license, PATCH /customers/{id}/entitlements
UsageGET /usage
Test purchasePOST /test-purchase, GET /test-purchase/{id}
MetaGET /tokens/verify, GET /confirmations/{id}, GET /openapi.json
AuthPOST /login/device, POST /login/device/token, POST /logout

See the OpenAPI document for full request and response schemas.