API Reference
The raw HTTP contract. Use this if you're integrating from a language without a ready-made SDK, or want to see exactly what the SDKs do under the hood.
Base URL: loading…
Conventions
- All requests and responses are JSON,
Content-Type: application/json. - The public license endpoints (
/api/v1/*) always return HTTP 200 for any request the server understood — the outcome (including failures like an expired key) is in the JSON body. Non-2xx is reserved for transport-level problems:400validation error,429rate limited,500unexpected error. - The private admin API (
/api/admin/*) powers the dashboard only and requires a signed-in, two-factor-protected session (see below).
Key types
Every license key has a key_type, chosen when it is created and shown in the dashboard, the CSV export, the check response and the automation API:
key_type | Behaviour |
|---|---|
device (default) | Binds to the hwid of the first device that activates it. Any other device gets hwid_mismatch until the key's device is reset. A device key can also allow several devices (max_devices: up to 3 on Starter, 5 on Pro, 15 on Business): each new device binds while there is room, and once the key is full a new device gets device_limit. |
public | Never binds. Once activated, it is valid on many devices at the same time, for anyone who has it — up to the seller's plan limit (10 devices on Free, 100 on Starter, 1,000 on Pro, no limit on Business); beyond it a new device gets device_limit. Devices that already used the key keep working. There is no hwid_mismatch and nothing to reset; revoke, restore and expiry work exactly as for device keys. |
The license protocol is identical for both: same requests (an hwid is still sent — it is recorded in the key's activity so you can see how many devices use it, but never enforced), same signed payload fields, same reason codes. Existing SDKs and integrations work with public keys unchanged. (A public license key has nothing to do with the Ed25519 public signing key below.)
GET /api/v1/public-key
No auth, no rate limit beyond the general limiter. Returns the Ed25519 public key used to sign every activate/verify response.
{ "publicKey": "base64-spki-der...", "algorithm": "ed25519" }
POST /api/v1/activate
Binds a key to a device on first use. Idempotent if called again with the same hwid. A public key never binds: every device's activation succeeds, and only the first one ever starts its “N days after activation” clock. Rate limited: 10 requests / 10 min per IP, and 20 / hour per key (the per-key limit doesn't apply to public keys — they are shared by design).
Request
{
"product": "YOUR_PRODUCT_ID",
"key": "7K2M9-QX3R8-2VBN5-8JH4C-T",
"hwid": "sha256-hex-of-a-machine-id" // see Security Model page
}
POST /api/v1/verify
Checks whether a previously-activated key is still valid on this device. Does not activate — a key that has never been activated returns reason: "not_activated" (SDKs handle this by calling /activate automatically). A public key verifies on any device once it has been activated at least once, by anyone. Rate limited: 30 requests / min per IP, and 120 / hour per key (the per-key limit doesn't apply to public keys).
Request
{
"product": "YOUR_PRODUCT_ID",
"key": "7K2M9-QX3R8-2VBN5-8JH4C-T",
"hwid": "sha256-hex-of-a-machine-id"
}
Response (both endpoints)
A signed envelope. See the Security Model page for exactly how to verify signature against payload.
{
"payload": {
"valid": true,
"reason": null,
"product_slug": "YOUR_PRODUCT_ID",
"key_id": 42,
"hwid_bound": true,
"status": "active",
"expires_at": "2027-01-01T00:00:00.000Z",
"issued_at": "2026-09-04T12:00:00.000Z",
"offline_grace_until": "2026-09-07T12:00:00.000Z"
},
"signature": "base64..."
}
For a public key, hwid_bound is always false; the payload has exactly the same fields for both key types.
reason values
| Value | Meaning |
|---|---|
null | Success — valid is true. |
not_found | No such key for that product (or the key belongs to a different product). |
malformed_key | The key string fails its checksum — almost always a typo. Never hits the database. |
revoked | The seller revoked this key. |
expired | Past its computed or fixed expiry date. |
hwid_mismatch | This key is already bound to a different device. Device keys only — a public key never returns it. |
not_activated | /verify only — this key has never been activated on any device yet (for a public key: by anyone yet), or — for a key allowed on several devices — not yet on this device while it still has room. Call /activate (the SDKs do). |
device_limit | The key is already used on as many devices as it allows (a multi-device key, or a public key at its plan's limit). Devices already using it keep working. |
suspended | The account that issued this key is suspended. Nothing it issued validates until it is restored. |
blocked | The service banned this device or IP address. Applies whatever key is presented, and is checked before anything else. |
POST /api/v1/check
Customer self-service lookup: what a buyer can see about their own key, with no device ID needed. Powers the “Check a license” tool on the home page. Rate limited to 20 requests per 10 minutes per IP.
Request
{ "key": "TVMWF-WFPKW-7AWNP-C5RE5-Z" }
Response
{
"found": true,
"product": { "name": "Your Product", "slug": "your-product" },
"status": "active", // unused | active | expired | revoked
"expires_at": "2027-01-01T00:00:00.000Z",
"duration_days": 30,
"days_left": 29,
"key_type": "device", // device | public
"max_devices": 1, // devices a device key may be used on
"device_bound": true,
"bound_at": "2026-09-04T12:00:00.000Z",
"reset": { "available": true, "pending": false }
}
An unknown, malformed or mistyped key always returns exactly { "found": false }. The device ID, notes and customer e-mail are never included, and the response is not signed (it is informational, not an entitlement — always use /verify to decide whether software may run).
POST /api/v1/reset-request
Lets a customer ask for their key to be moved to a new computer. Resetting a device is never self-service — a leaked key would let anyone unbind it — so this only files a request that an admin approves or dismisses in the dashboard. Limits: 5 per hour per IP, 3 per day per key, one open request per key.
{ "key": "TVMWF-WFPKW-7AWNP-C5RE5-Z", "contact": "[email protected]", "message": "New laptop" }
The answer is always { "ok": true } — whether or not a request was recorded — so the endpoint can’t be used to discover which keys exist. contact and message are optional. A public key is never bound to a device, so a request for one is not recorded.
Dashboard API
The dashboards talk to private APIs under /api/account/* (your account) and /api/admin/* (the service operator). They are not public integration surfaces and are deliberately hard to script:
- Requests need a signed-in browser session (an opaque, HttpOnly, same-site cookie) — there are no long-lived passwords or keys in headers.
- Two-factor authentication is available to everyone, sessions expire when idle, and sensitive actions ask for your password and a fresh code again.
- Cross-site requests are refused, every account only ever sees its own data, and sign-ins and changes are written to a history you can read.
Your software only needs the public endpoints above. To create keys automatically — say from a checkout webhook — use the token-based automation API below (see also Automation API).
Automation API
For scripts, checkout webhooks and bots that need to create or manage keys. It uses API tokens instead of passwords: create one in the dashboard under Settings → API tokens, choose exactly the permissions it needs, and optionally pin it to your server’s IP or give it an expiry. A token is shown once, stored only as a hash, and can be revoked instantly. Base path: /api/automation/v1.
Authorization: Bearer lk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
| Permission | Allows |
|---|---|
products:read | GET /products |
keys:create | POST /keys — up to 100 keys per call |
keys:read | GET /keys/:key — status, expiry, whether it is bound, customer e-mail and notes |
keys:revoke | POST /keys/:key/revoke and /restore |
keys:reset | POST /keys/:key/reset-device |
Generate keys from a webhook
curl -X POST .../api/automation/v1/keys -H "Authorization: Bearer $LICENSE_TOKEN" -H "Content-Type: application/json" -d '{"product": "your-product", "count": 1, "durationDays": 30, "customerEmail": "[email protected]"}'
# → 201 { "keys": [ { "key": "TVMWF-…", "status": "unused", "duration_days": 30, "key_type": "device", … } ] }
Add "keyType": "public" to create public keys (no device binding); leave it out, or send "device", for the usual device-locked keys. Add "maxDevices": 3 to let each key be used on up to that many devices (within your plan). Every key in a response carries its key_type and max_devices.
Errors use the same shape as the rest of the API: 401 INVALID_TOKEN (missing, wrong, expired, revoked or wrong-network token), 403 INSUFFICIENT_SCOPE, 404, 400 for invalid input, 409 PUBLIC_KEY for a device reset on a public key (it has no device), 429 when throttled. Everything a token does is recorded in the audit log under its name. Tokens can never reach admin accounts, settings, other tokens or the blocklist.
Two more errors relate to your account: 402 PLAN_LIMIT when creating something would exceed your plan (the body says which limit), and 403 ACCOUNT_SUSPENDED when the account that owns the token is suspended. See Accounts & plans.