Documentation menu▾

API reference

Endpoints, authentication, the response format, error codes, idempotency and retry rules for the Keygate license API and admin API.

This page describes the rules every endpoint follows and lists the endpoints your app and your servers will use. The full machine readable description of the license API is the OpenAPI file in the repository, which you can load into Postman, Insomnia or a client generator.

Base URL

All endpoints live under your install's address:

https://licenses.example.com/api/v1

Requests and responses are JSON. Field names are snake_case, timestamps are RFC 3339 in UTC, and an empty list is always [], never null.

Authentication

EndpointsCredential
/license/*None. The license key in the request body is the credential.
/releases/*None, unless the product requires a license on its feed.
/admin/*An API key: Authorization: Bearer kg_live_...
/portal/*A signed in customer's session cookie.

Create API keys under API Keys in the dashboard and give each one only the scope it needs:

  • admin can do everything an admin can do in the dashboard.
  • licenses:write can create licenses and manage their lifecycle, which is what a billing backend needs.
  • releases:write can upload and publish releases, for CI.

A key can also be limited to one product; reaching for another product's data then answers 403 PRODUCT_SCOPE_MISMATCH. A key with no scope can do nothing.

Responses

Every response has the same shape, whether it succeeds or not:

json
{ "success": true,  "data": { ... } }
{ "success": false, "error": { "code": "LICENSE_NOT_FOUND", "message": "license not found" } }

Exactly one of data and error is present. A few error codes add a details object, for example ACTIVATION_LIMIT includes {"current": 3, "max": 3}. The update feeds are the exception to the envelope: they return the format the updater expects when they succeed.

Errors and retries

Each error code always comes with the same HTTP status, so you can branch on the code and decide whether to retry from the status:

StatusWhat to do
429Back off and retry. The exception is QUOTA_EXCEEDED: a spent usage quota does not come back by waiting.
502, 503Back off and retry, except codes ending in _NOT_CONFIGURED or _DISABLED, which mean the feature is turned off on this install.
500INTERNAL_ERROR. Worth one retry. The details are in the server log.
Other 4xxThe request itself is the problem. Retrying it unchanged will not help.

Idempotency

Three endpoints accept an Idempotency-Key header: activate, usage and floating checkout. Send the same key with every retry of one request. A retry gets the first response back, marked with an Idempotent-Replayed: true header, for 24 hours.

CodeStatusMeaning
IDEMPOTENCY_IN_FLIGHT409The first request is still running. Retry shortly.
IDEMPOTENCY_KEY_CONFLICT422The same key was used with a different body.
INVALID_IDEMPOTENCY_KEY400The key is longer than 256 characters or contains control characters.

License API

Called by your app. All are POST with a JSON body containing at least license_key, except pubkey.

EndpointPurposeGuide
/license/activateRegister a device or user on a license.Activating
/license/verifyCheck a license and get a fresh offline token.Verifying
/license/deactivateFree a device or user.Deactivating
GET /license/pubkeyThe Ed25519 key that verifies offline tokens.Working offline
/license/entitlementsThe plan's features and remaining quotas.Features
/license/usageRecord usage of a metered feature.Usage
/license/usage/statusRead a quota without changing it.Usage
/license/floating/checkoutTake a floating seat.Floating
/license/floating/heartbeatKeep a floating seat.Floating
/license/floating/checkinGive a floating seat back.Floating
/license/downloadA short lived download link for a release.Downloads

Brute force protection

Failed license calls are counted per IP address. After five failures within five minutes, that address is locked out and receives 429 LOCKED_OUT: for 30 seconds the first time, then twice as long each time, up to 30 minutes. Verify answers 404 LICENSE_NOT_FOUND for every kind of failure, so a valid key cannot be found by watching the responses.

Update feeds

EndpointReturns
GET /releases/:slug/feed.xmlA Sparkle appcast.
GET /releases/:slug/velopack/:platform/releases.:channel.jsonA Velopack feed. Velopack is given the base address and builds this one.
GET /releases/:slug/upgrade.jsonA Tauri update manifest, or 204 when there is nothing newer.

All three take platform (required), channel and, for the first two, limit. See Shipping updates.

Admin API

Everything the dashboard does goes through the admin API, so anything you can click you can also automate. The most common job is creating a license from your own backend, for example when a sale happens outside Stripe:

bash
curl -X POST https://licenses.example.com/api/v1/admin/licenses \
  -H "Authorization: Bearer kg_live_..." \
  -H "Content-Type: application/json" \
  -d '{"product_id":"...","plan_id":"...","email":"[email protected]",
       "external_customer_id":"cus_1042"}'

external_customer_id is optional and links the license to the customer in your own system. Find the licenses for that customer again with GET /admin/licenses?external_customer_id=cus_1042. Licenses are suspended, reinstated and revoked with POST /admin/licenses/:id/suspend, /reinstate and /revoke.

Pagination

Admin lists are paginated with limit and offset:

json
GET /api/v1/admin/licenses?limit=50&offset=0

{ "success": true, "data": {
    "licenses": [ ... ],
    "total": 1234,
    "limit": 50,
    "offset": 0 } }

The default page size is 50 and the largest is 200. Asking for more is not an error; you get 200, and the limit in the answer says so. Always work out the number of pages from the response, not from what you asked for.

Last updated October 4, 2026