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/v1Requests and responses are JSON. Field names are snake_case, timestamps are RFC 3339 in UTC, and an empty list is always [], never null.
Authentication
| Endpoints | Credential |
|---|---|
/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:
admincan do everything an admin can do in the dashboard.licenses:writecan create licenses and manage their lifecycle, which is what a billing backend needs.releases:writecan 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:
{ "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:
| Status | What to do |
|---|---|
| 429 | Back off and retry. The exception is QUOTA_EXCEEDED: a spent usage quota does not come back by waiting. |
| 502, 503 | Back off and retry, except codes ending in _NOT_CONFIGURED or _DISABLED, which mean the feature is turned off on this install. |
| 500 | INTERNAL_ERROR. Worth one retry. The details are in the server log. |
| Other 4xx | The 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.
| Code | Status | Meaning |
|---|---|---|
IDEMPOTENCY_IN_FLIGHT | 409 | The first request is still running. Retry shortly. |
IDEMPOTENCY_KEY_CONFLICT | 422 | The same key was used with a different body. |
INVALID_IDEMPOTENCY_KEY | 400 | The 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.
| Endpoint | Purpose | Guide |
|---|---|---|
/license/activate | Register a device or user on a license. | Activating |
/license/verify | Check a license and get a fresh offline token. | Verifying |
/license/deactivate | Free a device or user. | Deactivating |
GET /license/pubkey | The Ed25519 key that verifies offline tokens. | Working offline |
/license/entitlements | The plan's features and remaining quotas. | Features |
/license/usage | Record usage of a metered feature. | Usage |
/license/usage/status | Read a quota without changing it. | Usage |
/license/floating/checkout | Take a floating seat. | Floating |
/license/floating/heartbeat | Keep a floating seat. | Floating |
/license/floating/checkin | Give a floating seat back. | Floating |
/license/download | A 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
| Endpoint | Returns |
|---|---|
GET /releases/:slug/feed.xml | A Sparkle appcast. |
GET /releases/:slug/velopack/:platform/releases.:channel.json | A Velopack feed. Velopack is given the base address and builds this one. |
GET /releases/:slug/upgrade.json | A 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:
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:
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