Server endpoints
The endpoints your backend calls with a secret key, to issue, read and change keys, free devices, report releases and make test licenses.
… stands for /v1/licenses. See also errors common to
every endpoint and rules the change endpoints share.
| Endpoint | Does |
|---|---|
| Issue and read | |
POST /v1/ | Issues a key, or several as one sale. |
GET /v1/ | Returns one license. |
GET /v1/ | Lists your licenses, with filters. |
GET /v1/ | Returns the license a Stripe checkout or a Paddle transaction created or changed. |
POST …/ | Returns a key's key file or license file. |
| Change a key | |
POST …/ | Moves its expiry later. |
POST …/ | Sets the versions it covers and its upgrade protection. |
POST …/ | Sets its device limit. |
POST …/ | Sets when its unused seats are freed. |
POST …/ | Sets who can activate it. |
…/ | Sets, removes, tops up, resets or spends a meter. |
POST …/ | Revokes it. |
| Devices | |
GET …/ | Lists the devices it is active on. |
DELETE …/ | Frees one device's seat. |
| Products | |
POST /v1/ | Reports a release. |
GET /v1/ | Lists your products and their license models. |
| Testing | |
POST /v1/ | Makes a test license in a chosen state. |
Authentication
-
Send
Authorization: Bearer <secret>, with a secret key from the dashboard's API keys page. - A missing, invalid or revoked secret key returns
401 unauthorized. - Each secret key can make 1,200 requests per minute, of which at most 600 may issue licenses or make test licenses (see Rate limits).
kg_live_: reaches every license in your account and issues live licenses.-
kg_test_: issues test licenses and reaches only those. A live license or a live checkout gets the same response as one that does not exist.
A license key in a path is case-insensitive, and responses return the key as it was issued.
Errors on every endpoint with :key
No license in your account has that key, or a test secret key asked for a live one.
Rules the change endpoints share
| Rule | Holds for |
|---|---|
| Sending what the key already has returns it unchanged, so retries are safe. | extend, devices, seat-release, user-lock, and setting a meter |
| The change appears in the key's history on the dashboard. | devices, seat-release, user-lock, every meter route (as made over the API), and each file download |
| A revoked key takes the change and stays revoked. | devices, seat-release, the meter routes except a spend (a revoked key refuses a spend) |
POST /v1/licenses
Issues a license key, or several as one sale.
Request
{ "product": "my-product", "modelId": "mod_…", "email": "[email protected]", "version": "2.4.1", "sendEmail": true} Required. The product's API ID.
The license model sold, by its ID (mod_ and 32 hex characters). It sets the type, expiry, device limit, grace, Offline days, Free unused seats, entitlements, versions and price.
perpetual, subscription, trial or timed, for a key sold under no model (see Keys sold under no model). Send modelId or model; if you send both, modelId takes precedence.
- A
timedone needsexpiresAt(400 term-requiredwithout it): only a timed model names a term. - It takes the device limit, grace, entitlements, Offline days and Free unused seats of the product's oldest model of that type, or 3 devices, no grace and no seat release if there is none.
- It has no price, and no expiry unless you send
expiresAt. - Its versions are its type's default: a perpetual key covers the version sold, or its first activation's, and a subscription or trial key covers every version.
The customer's email.
Overrides the model's device limit: a whole number.
Overrides the model's Free unused seats: the days a device may go without checking in before its seat is freed, or null for never (see Free unused seats).
Overrides the model's expiry, in Unix milliseconds. Under a timed model it replaces the term, a term counted from first use included (see Timed models).
true to email the key (see Notes). false by default.
The same as the Idempotency-Key header, which takes precedence when both are sent.
The version sold: "2.4.1", "v2", 2, "2.4.1-beta+5". Only the major version counts, and 0.x counts as 1.
The key's own range, overriding the model's. Send both or neither: whole numbers, with maxMajor null for "and later".
Overrides the model's upgrade protection, in months of 30 days each. Applies only to a range with a highest version.
Replaces the model's entire set. null or {} for none. Each must be defined on the product's Entitlements tab, with a value of its type (400 unknown-entitlement or 400 entitlement-type otherwise), but for one its model grants as it is. See the rules.
Replaces what the model counts, the whole set by meter code: { "exports": { "uses": 1000, "resets": "never", "offline": null } }. {} counts nothing. See Metered uses.
- At most 8, each with whole
usesfrom 0 to 1,000,000,000. resetsisnever(the default) orperiod, a meter that comes back each billing period, on a subscription only.offlineis the allowance each device may spend while KeyGrant cannot be reached,nullfor the default.
How many keys to issue as one sale, each with the same terms. Without it a request issues one key, whatever the model's Keys per sale.
- A key issued here records no amount (only a paid checkout does): its license page says none was recorded, and it never counts toward what a customer has paid.
Response
201with{ "key", "emailed" }.-
With a
count:{ "key", "keys", "emailed" },keythe first andkeysevery key, first to last.sendEmailsends them in one email. emailedistrueonly if the email was sent.- No license object: fetch it with
GET /v1/licenses/:key.
Errors
No product of yours has that API ID.
The product is archived.
A version, range or upgradeMonths that is invalid or inconsistent.
A seatReleaseDays fewer than the key's Offline days.
The idempotency key was first used for another product, with a secret key of the other mode (live or test), or for another count.
Issuing this live license would take you past your plan's limit on active licenses, counted across all your tenants. A count is refused whole, never cut to what fits. The response includes dimension, limit, usage, plan and scope (owner: the usage is across all your tenants, under the plan that covers them).
Your account already has 25 active test licenses, the most it can hold, or the keys a count asks for would take it past 25: a count is refused whole. The response includes limit and usage.
modelId is not a model of that product.
modelId is an upgrade offer (an edition, devices or uses), which changes a key the customer already holds and issues none.
A code in meters is not one of the product's meters. The response names it as meter.
The key cannot carry its meters:
meter-license-file: it is sent as a license file, which never checks in.meter-period-subscription-only: a meter comes back each billing period on a key that is not a subscription's.meter-upgrade-offer: the model is an upgrade offer, which issues no key.
The key would have no term, or would run out before it was made (see Timed models):
term-required: a baretimedmodelwith noexpiresAt.term-ended: a timed model whose end date has passed, with noexpiresAtof its own.
A live key of a model that delivers files (a key file or a license file), or whose model locks Who can activate (see Who can activate), needs email: the file is made out to it, and the lock compares with it.
A live key's upgradeMonths above 0 under a model that delivers license files: a license file never checks in, so it cannot carry upgrade protection. A test key is issued as a typed key, and takes it.
The key could not be created. Retry with the same idempotency key.
Notes
-
sendEmailemails the key with the product's delivery template only when the key has an email, the product's License delivery tab is set to KeyGrant sends them, and its Key delivery email is on (see Key delivery email). - A live sale that names a
versionmoves the product's current version up to it.
A retry with the same idempotency key is safe:
-
For the same product and with a secret key of the same mode, it returns
200with the original license key and"emailed": false(and, for acount, every key it issued askeys). Nothing else happens. -
Its other fields are not compared, except
count: a retry naming another count returns409 idempotency-key-reused. - Once the product is archived, a retry returns
409 product-disabled. - Idempotency keys are kept for the life of their license.
GET /v1/licenses/:key
Returns one license.
Response
200 with { license } (see
The license object).
Errors
Only 404 unknown-license.
GET /v1/licenses
Lists your licenses, newest first.
Request
Query parameters, every one optional. One with an empty value is treated as absent.
A product's API ID.
The customer's full address, case-insensitive. Partial addresses do not match.
active (every currently valid key, including trial keys), trial, expired, revoked, or all (the default).
How many licenses a page holds.
The nextCursor of the previous page.
Response
{ "licenses": [ { "key": "MYPROD-9K4T-2M8P-XQ7V-H3DW", "...": "..." } ], "nextCursor": "1790899200000:MYPROD-9K4T-2M8P-XQ7V-H3DW"} nextCursorisnullon the last page; treat it as opaque.- Pages stay stable while new keys are created.
Errors
An invalid or unknown parameter. It is refused instead of being ignored, so a misspelled filter never returns every license.
The product is an API ID that is not one of your products.
GET /v1/checkouts/:session/license
Returns the license a Stripe checkout or a Paddle transaction created, or the license an upgrade checkout
changed when the checkout named that key in its client_reference_id.
After a checkout shows the page that polls it.
Request
No body. :session is the Checkout Session ID (cs_…, up to 255 characters) or the
Paddle transaction ID (txn_…).
Response
200with{ license }.-
A checkout that created a sale of several keys returns its first key as
licenseand every key, first to last, aslicenses.
Errors
The license does not exist yet. Retry: the webhook that creates the key can land a moment after the buyer does.
The checkout upgraded a key the buyer did not name, such as one found by an activation ID or the buyer's email address (see How KeyGrant finds the key).
KeyGrant held the sale: it could not place it on a key, so no key is coming (see Held sales).
A held sale was refunded since.
The ID is not a Checkout Session ID or a Paddle transaction ID.
POST /v1/licenses/:key/file
Returns the file of a key sent as a key file or a license file, for a backend that sends its own mail.
Request
The body is optional: { "resign": true } signs it again with the key that signs now,
after a signing key leaked.
Response
200 with
{ file: { id, kind, kid, issuedAt, signedAt, filename, mediaType, content } }:
save content under filename, or attach it to your own mail.
Errors
The key is a typed key, not sent as a file.
The key has no email address to make the file out to.
The key is revoked.
The key is sent as a license file and has an end date.
Notes
The file is the one already made while the key says the same, and is signed again when the key changed.
POST /v1/licenses/:key/extend
Moves the key's expiry later.
Request
{ "expiresAt": <Unix milliseconds> }, the new expiry.
Response
200 with the updated { license }.
Errors
The key is revoked, and extending does not reinstate it. That includes a subscription key revoked after its grace period, and a trial key revoked after its expiry.
The key has no expiry to move: a perpetual key issued without one, a key issued with only a model type and no expiry, or a Stripe subscription whose first period Stripe has not reported yet.
A timed key still waiting for its first activation: its term has not started, so it has no end yet to move (see Timed models).
expiresAt is not a whole number, is already past, or is earlier than the key's current expiry.
A test license already past its 30-day limit.
The key had expired, and bringing it back would pass your plan's limit, counted across all your tenants (or this tenant's test license limit).
Notes
A test license cannot be extended past 30 days after it was created.
POST /v1/licenses/:key/versions
Sets which major versions the key covers, and its upgrade protection.
Request
{ "minMajor", "maxMajor", "upgradeUntil" }:
Required, both, every time: whole numbers, with maxMajor null for "and later". A range may include a version you have not released yet.
Optional. When upgrade protection ends, in Unix milliseconds, or null to end it now.
Response
200 with { license }.
Errors
A missing bound, or a highest version below the lowest.
An upgradeUntil that opens or lengthens a window on a live key sent as a license file, which never checks in. Ending or shortening one it holds is taken.
POST /v1/licenses/:key/devices
Sets how many devices the key can be active on.
Request
{ "deviceLimit": 5 }:
Required. How many devices the key can be active on, a whole number. It replaces the limit the key has, whether that came from its license model or from the deviceLimit it was issued with.
Response
200 with { license }. Its activeDevices says how many
devices are active now.
Errors
A missing limit, or one that is not a whole number from 1 to 1,000.
Notes
Lowering the limit below the devices the key is active on releases none of them. They keep working,
and a new device's activation is refused with limit until fewer devices than the limit are
active.
POST /v1/licenses/:key/seat-release
Sets when the key frees the seat of a device that stopped checking in (see Free unused seats).
Request
{ "seatReleaseDays": 30 }:
Required. How many days a device of the key may go without checking in before its seat is freed, a whole number, or null to free none. It replaces what the key has, whether that came from its license model or from the seatReleaseDays it was issued with.
Response
200 with { license }.
Errors
A missing value, or one that is not a whole number from 1 to 365.
Fewer days than the key's Offline days.
Notes
Turning it on, or shortening it, frees a device already unseen that long the next night.
POST /v1/licenses/:key/user-lock
Sets who may activate a new device on the key (see Who can activate).
Request
{ "userLock": "email" }:
Required. Who may activate a new device on the key. It replaces what the key has, whether that came from its license model or an earlier change.
off: anyone with the key.email: whoever types the email it was sold to.code: whoever types that email and a code emailed to it.
Response
200 with { license }, whose userLock is the new value.
Errors
A missing or unknown value.
The key has no email, and a lock compares with it.
The key is sent as a license file.
The key is a test license.
Notes
offis always taken.- A device that already holds a seat is never asked, so the change affects only devices activated afterwards.
Meters: /v1/licenses/:key/meters
Five routes for a key whose license model counts uses (see Metered uses). What they share:
- Each responds
200with{ license }, but a spend. - Every route but the first answers
404 unknown-meterwhen the key does not hold the meter. -
No change sends a webhook. A spend that takes a meter's last use sends
meter.used_up.
POST …/:key/meters/:meter
Sets the meter's settings on the key, and adds the meter when the key lacks it and the product has it.
Request
{ uses?, resets?, offline? }, offline null for the default. A
new meter needs its uses.
Errors
The product has no meter of that code.
resets is period on a key that is not a subscription's.
The key is sent as a license file.
A body that does not fit, or a new meter without uses.
The key already holds 8 meters.
DELETE …/:key/meters/:meter
Takes the meter off the key. The app's use() of it is then no-meter; its spends
stay in the key's history. No body.
POST …/:key/meters/:meter/add
Adds uses on top. They carry across billing periods until spent. An Idempotency-Key header
is required: a replay adds nothing and answers the license as it stands.
Request
{ "uses": 500 }, 1 to 1,000,000,000.
Errors
No Idempotency-Key header.
The same idempotency key with another body.
The meter would pass 1,000,000,000.
POST …/:key/meters/:meter/reset
Starts a new period by hand. An Idempotency-Key header is required, as for an add, so a
replay resets nothing twice. No body.
Errors
409 meter-not-periodic: the meter does not come back each billing period. Also
400 idempotency-key-required without the header.
POST …/:key/meters/:meter/use
A spend made by your backend, such as a cloud render the key pays for. Exact and idempotent by
id, as an app's spend is. Your secret key is the proof, so no seat is asked.
Request
{ n?, id }: n 1 by default, and id 16 to 64 base64url
characters, unique for the key.
Response
200 with { meter }, the meter as the license object shows it.
Errors
Fewer uses left than asked. The response includes left.
The key is revoked or has expired.
POST /v1/licenses/:key/revoke
Revokes the key.
Request
Optional. refund, chargeback, subscription_canceled or manual (the default).
Response
200 with { "ok": true }.
Errors
Only 404 unknown-license.
Notes
- The key is refused at the app's next check-in, and its devices keep their seats (see When a revoke takes effect).
-
Revoking a key that is already revoked also returns
200, records this call's time and reason, and sendslicense.revokedagain, except a key a refund or a lost dispute revoked, which keeps that revoke: the call changes, records and sends nothing. - The API cannot reinstate a key.
GET /v1/licenses/:key/activations
Lists the devices the key is active on.
Response
200 with { "key", "activations": [ ... ] }, the key as stored and each
device with:
The activation id, which DELETE frees.
The device's fingerprint, as device.activated carries it.
What the app's activation sent: unchecked text, null when it sent none.
Its last check-in, or null if it never checked in.
When it activated.
The time after which the first nightly pass frees the seat, unless the device checks in first.
nullwhile the key does not free unused seats.nullfor a seat that never checked in.nullfor adevice-limittest license's placeholder seat.
- The most recently seen device comes first; a device never seen comes by when it activated.
- Every time is in Unix milliseconds.
- A key on no device returns an empty list.
Errors
Only 404 unknown-license.
DELETE /v1/licenses/:key/activations/:id
Frees one device's seat.
Request
No body. :id is the activation id (GET …/activations lists
the ids).
Response
200 with { "released": 1 }, or { "released": 0 }
when :id is not a seat on that key.
Errors
Only 404 unknown-license.
POST /v1/products/:slug/releases
Reports a release: moves the product's current version up to its major, never down (see Your product's current version).
Request
{ "version" }:
Required. In the same format as version on POST /v1/licenses.
Response
200 with { "product", "currentMajor" }, the current version after the
call.
Errors
The version cannot be parsed.
A test secret key, or a product in another account.
GET /v1/products
Lists every product of your account, oldest first, each with its license models: the API IDs and the
modelId an issue or a test license needs,
for a build script that holds a secret key.
Request
No body and no parameters. It is not paged: it returns every product of the account.
Response
200 with { products }:
{ "products": [ { "slug": "my-product", "name": "My Product", "keyPrefix": "MYPROD", "currentMajor": 2, "trialDays": 14, "trialEntitlements": {}, "archived": false, "createdAt": 1790899200000, "meters": [ { "code": "exports", "name": "Exports", "description": null } ], "models": [ { "id": "mod_5c1e0a7b9d2f4e8a6b3c1d0e9f8a7b6c", "name": "Perpetual", "type": "perpetual", "priceCents": 7900, "deviceLimit": 3, "delivery": "key", "userLock": "off", "versions": "at_purchase", "meters": [ { "code": "exports", "uses": 1000, "resets": "never", "offline": null } ], "entitlements": {}, "...": "..." } ] } ]} Each product has:
The product's API ID: what the SDKs, the license routes and /pubkey name it by.
The product's name.
Its license keys' prefix: MYPROD of MYPROD-9K4T-2M8P-XQ7V-H3DW.
The highest major version it ships, which caps what upgrade protection keeps. A release moves it up.
A device trial's length in days.
What a device trial's leases grant.
true for an archived product, which issues no keys (409 product-disabled). Its keys keep working.
When it was created, in Unix milliseconds.
What its apps count: a list of { code, name, description }, code being what use() passes and description text or null. See Metered uses.
Its license models, in the order the dashboard's Models tab lists them.
The license models
Each entry of models holds the terms of one model, as the dashboard's Models
tab sets them (see License models):
The model's ID: mod_ and 32 hex characters. It is what modelId takes on issue and on a test license.
The model's name.
perpetual, subscription, trial or timed.
Text, or null.
true for an archived model, which sells nothing new. Its keys keep their terms.
The price in minor units (null for a free or trial model) and its currency.
monthly or yearly for a subscription, null otherwise.
How many devices a key sold under it can be active on.
Days a key keeps working after it expires, for a subscription model; null is none.
A trial model's length in days, null otherwise.
Offline days: each lease's length, 1 to 90, or null for the type's default.
Free unused seats: the days a device may go without a check-in before its seat is freed, never fewer than its Offline days; null is off. See Free unused seats.
How many keys one sale of it makes.
How its keys are delivered: key, key_file or license_file.
Who may activate a new device on its keys: off, email or code. See Who can activate.
Which majors its keys cover: at_purchase (the version sold only), from_purchase (that and every later one), all or range.
A range model's lowest major and its highest, maxMajor null for "and later".
Months of upgrade protection a key gets, 0 to 120.
true for an upgrade offer: buying it changes a key the buyer holds rather than issuing one.
An edition upgrade's entitlements: the set a key must hold, and the set that replaces it. {} on any other model.
A device upgrade's devices per unit. 0 on any other model.
A uses upgrade's uses per unit, by meter code. {} on any other model. See Sell more uses.
What its keys count: a list of { code, uses, resets, offline }, each picked from the product's meters, its fields as an issue's meters takes them.
What its keys grant.
A timed model's term: termLength in termUnit (days or months), counted from termStarts (purchase or activation); or its fixed end date, endsAt. All null on any other type.
When it was created, in Unix milliseconds.
Notes
- A live key and a test key may both call it: a product and its terms are no license, so a test key reaches nothing a live customer holds through it.
- It holds no key material: a product's public key is GET /pubkey.
POST /v1/test-licenses
Makes a test license in one of the states an app must handle, as the dashboard's Create test license does (see Testing your integration).
- It is signed with the product's real key and carries
test: true. - It lasts 30 days at most and is never emailed.
- It counts toward the account's 25 active test licenses, never toward its plan.
- Its history says it came from the API.
Request
{ "product": "my-product", "scenario": "device-limit"} Required. The product's API ID.
Required. The state the license is made in: one of the six scenarios.
The license model, by its ID.
- Without it: the product's oldest model that is not archived and issues keys (never an upgrade offer; for
used-up, the oldest that counts uses), or, if it has none, a perpetual key sold under no model. trial-endingignoresmodelIdand uses the product's trial model, or, if it has none, atrialkey sold under no model.
The version it is for, in the same format as version on issue. Without it, a key that needs a version takes its first activation's.
The same as the Idempotency-Key header, which takes precedence when both are sent.
Scenarios
A valid key.
A trial that ends in 24 hours.
Expired an hour ago.
Revoked when it is created.
One device allowed and already taken by a placeholder, so activating is refused.
Its first meter has no uses left. It needs a model that counts uses.
Response
201 with { key, scenario, license }, the license as
GET /v1/licenses/:key shows it (see
The license object):
{ "key": "MYPROD-9K4T-2M8P-XQ7V-H3DW", "scenario": "device-limit", "license": { "key": "MYPROD-9K4T-2M8P-XQ7V-H3DW", "...": "..." }} Errors
No product of yours has that API ID.
modelId is not a model of that product.
The product is archived.
The body does not fit: a missing product, or a scenario that is not one of the six.
A version that cannot be read.
modelId is an upgrade offer, which changes a key the customer already holds and issues none.
modelId is a timed model whose end date has passed (the expired scenario sets its own expiry, so it is never refused for this).
The used-up scenario, with a model that counts nothing.
The idempotency key was first used for another product, for a live license, for another scenario, or for an issue of several keys.
Your account already has 25 active test licenses, the most it can hold. The response includes limit and usage. An expired or revoked test license takes none of the 25.
Past the key's 600 requests a minute for issuing (shared with POST /v1/licenses), or the account's 200 test licenses a day (see Rate limits). The response includes retryAfter.
The key's request could not be counted against its limit, so nothing was made. Retry after retryAfter seconds.
The key could not be created. Retry with the same idempotency key.
Notes
-
A request with the same idempotency key as an earlier one returns
200with the key first made, and nothing else happens. - A live key and a test key may both call it, and a test key reaches the license afterwards, as it can reach any test license.
- The 200 test licenses a day count from midnight UTC, and a request that replays an idempotency key spends none of them.