Documentation menu▾

Activating licenses in your app

Activate a license key from your app, verify it on startup, keep working offline with a signed Ed25519 token and free a device when the user moves.

Your app needs three calls to the license API: activate once when the user enters a key, verify when the app starts, and deactivate when the user moves to another computer. None of them needs an API key. The license key is the credential, so there is no secret to hide inside your app.

All three take JSON and are served under /api/v1/license on your Keygate address. For complete code in a specific framework, see the guides for Electron, Tauri, .NET, Python and Swift.

How it works

For why Keygate uses random keys plus signed tokens instead of keys your app checks by itself, see How license keys work.

  1. The user types their license key. Your app calls activate with the key and an identifier for the device. Keygate records the activation and returns a signed token.
  2. Your app saves the key and the token. On every start it checks the token locally. If the token is still within its check in interval, the app runs without touching the network.
  3. When the token is due, the app calls verify and gets a fresh token, along with the current plan and features. If the license was canceled or revoked in the meantime, the answer says so.
  4. When the user switches computers, the app calls deactivate and the slot is free for the new machine.

Choosing an identifier

The identifier tells Keygate which device an activation belongs to. It must stay the same for the same machine, or every restart would look like a new device and use up a slot. Good sources are the operating system's machine ID: IOPlatformUUID on macOS, the MachineGuid registry value on Windows, /etc/machine-id on Linux. Hashing the value before you send it is a good habit, since you only need it to be stable, not readable.

If you license by person instead of by machine, send the user's email as the identifier. Activation works on desktop and hybrid products; a SaaS product counts people as seats instead.

Activating

bash
curl -X POST https://licenses.example.com/api/v1/license/activate \
  -H "Content-Type: application/json" \
  -d '{
    "license_key": "KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789",
    "identifier": "9f2c41e07b6a",
    "label": "Office iMac"
  }'
FieldRequiredNotes
license_keyYesThe key the customer entered.
identifierYesStable ID of the device, or the user's email.
identifier_typeNodevice (the default) or user, stored with the activation as a label.
labelNoA name the customer will recognize in the portal, such as the computer's name.

A successful answer:

json
{
  "success": true,
  "data": {
    "status": "activated",
    "license_id": "4f0c6c1e-2a7b-4d4e-9a55-0d3b7e1f2c11",
    "token": "eyJsaWQiOiI0ZjBj...In0.Xk3m9QJ..."
  }
}

Calling activate again from a device that is already active returns already_activated and does not use another slot, so it is safe to call whenever you are unsure.

Verifying on startup

bash
curl -X POST https://licenses.example.com/api/v1/license/verify \
  -H "Content-Type: application/json" \
  -d '{"license_key":"KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789","identifier":"9f2c41e07b6a"}'
json
{
  "success": true,
  "data": {
    "status": "active",
    "plan_id": "c2b7...",
    "plan_name": "Pro",
    "valid_until": "2027-10-04T00:00:00Z",
    "features": { "export_pdf": true, "projects": "50" },
    "grace_days": 7,
    "token": "eyJsaWQiOiI0ZjBj...In0.Q8vR2..."
  }
}

valid_until is missing for licenses that never expire. A perpetual license with an update period also returns updates_until, the last date whose releases it may install.

Working offline

Every activate and verify answer contains a token signed with Ed25519. Your app can check it without a network connection, which is what lets it start on a plane or behind a firewall.

Get the public key

Fetch the verification key once and ship it inside your app. It only changes if you change LICENSE_SIGNING_KEY on the server.

bash
curl https://licenses.example.com/api/v1/license/pubkey
{"success":true,"data":{"algorithm":"ed25519","format":"hex","public_key":"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"}}

Check the token

A token is two base64url strings joined by a dot: the payload and its signature. The signature covers the payload exactly as it appears in the token, before decoding. In Node.js:

javascript
import { createPublicKey, verify } from "node:crypto";

const PUBLIC_KEY_HEX = "3b6a27bc..."; // from /api/v1/license/pubkey

// Wrap the raw 32 byte key in the DER header Node expects for Ed25519.
const publicKey = createPublicKey({
  key: Buffer.concat([
    Buffer.from("302a300506032b6570032100", "hex"),
    Buffer.from(PUBLIC_KEY_HEX, "hex"),
  ]),
  format: "der",
  type: "spki",
});

export function readToken(token, identifier) {
  const [payload, signature] = token.split(".");
  const valid = verify(null, Buffer.from(payload), publicKey, Buffer.from(signature, "base64url"));
  if (!valid) return null;

  const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
  if (claims.did !== identifier) return null; // issued to another device
  return claims;
}

The same in Go:

go
func readToken(token, identifier string, publicKey ed25519.PublicKey) (map[string]any, bool) {
	payload, signature, ok := strings.Cut(token, ".")
	if !ok {
		return nil, false
	}
	sig, err := base64.RawURLEncoding.DecodeString(signature)
	if err != nil || !ed25519.Verify(publicKey, []byte(payload), sig) {
		return nil, false
	}
	raw, err := base64.RawURLEncoding.DecodeString(payload)
	if err != nil {
		return nil, false
	}
	var claims map[string]any
	if json.Unmarshal(raw, &claims) != nil || claims["did"] != identifier {
		return nil, false
	}
	return claims, true
}

What the token contains

All times are Unix seconds.

FieldMeaning
lidLicense ID. pid and pln are the product and plan IDs.
stsLicense status when the token was issued.
didThe identifier the token was issued to.
ftrThe plan's features, the same as in the verify answer.
iatWhen the token was issued. exp is when the app should check in again: the check in interval from now, or the end of the license plus its grace days if that comes sooner.
vunWhen the license itself ends. Missing means it never does.
grcGrace days after vun.
updEnd of the update period, for perpetual licenses that have one. Install only releases published before it.

Putting it together

  • If the token is valid and exp is in the future, run normally.
  • If exp has passed, call verify. On success, save the new token and run.
  • If verify cannot reach the server, decide how strict to be. Most apps give a few days of warning before locking features, because flaky networks are far more common than pirates.
  • If verify answers 404, the license no longer works on this device.

How long the token lasts is the check in interval on the plan. A week is a reasonable default; shorter intervals make cancellations take effect sooner, longer ones suit apps that are often offline.

Deactivating

bash
curl -X POST https://licenses.example.com/api/v1/license/deactivate \
  -H "Content-Type: application/json" \
  -d '{"license_key":"KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789","identifier":"9f2c41e07b6a"}'

Offer this in your app's settings, for example as a Deactivate this computer button, and delete the stored key and token afterwards. Customers who no longer have the old machine can free the slot themselves in the customer portal, so a lost laptop does not turn into a support ticket.

Handling errors

Errors come back in the same envelope with a code you can branch on. These are the ones an app usually needs to handle:

StatusCodeWhat to tell the user
404LICENSE_NOT_FOUNDThe key is wrong, or the license no longer works here. Ask for a key again.
409ACTIVATION_LIMITAll devices are in use. The answer includes details.current and details.max. Point the user to the customer portal to free one.
403LICENSE_EXPIRED, LICENSE_SUSPENDED, LICENSE_REVOKED, LICENSE_CANCELEDReturned by activate when the license exists but cannot be used. Show the reason.
404FEATURE_NOT_AVAILABLEThe product is a SaaS product, which has no device activations. Use a desktop or hybrid product.
429LOCKED_OUTToo many failed attempts from this network. Wait a little and try again.

The API reference has the full rules for errors and retries.

Retries

Activation is safe to repeat, but if your app retries after a timeout, send an Idempotency-Key header. Generate one random value when the user starts the activation and send the same value with every retry of it. A retry with the same key gets the first answer back instead of being processed twice. Keys are remembered for 24 hours.

bash
curl -X POST https://licenses.example.com/api/v1/license/activate \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1d8a52-3c0e-4b7a-9f2e-1a4c7d9e0b35" \
  -d '{"license_key":"KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789","identifier":"9f2c41e07b6a"}'

Last updated October 4, 2026