Client endpoints
The endpoints your app calls, with no API key. An SDK makes these calls for you; this page is for integrating without one.
| Endpoint | Does |
|---|---|
GET /pubkey | Returns the public keys that verify leases. |
POST /activate | Takes a seat for this device and returns a lease. |
POST /validate | Renews the lease of a device that holds a seat. |
POST /use | Spends uses of a meter. |
POST /deactivate | Frees a seat. |
POST /trial | Starts or resumes this device's trial. |
POST /recover | Emails a customer their keys. |
POST /pickup | Emails a code to restore a license by email. |
POST /pickup/ | Returns the email's keys for the code. |
-
Every path is under
/v1/products/:slug/, where:slugis the product's API ID. - Errors common to every endpoint are on REST API, and every limit is in Rate limits.
GET /pubkey
Returns the product's public keys, for verifying leases. See Rotating the signing key for when keys are staged and retired.
Request
No body.
Response
200 with { publicJwk, keys }:
The key that signs leases now, as a JWK with kty OKP, crv Ed25519, alg EdDSA and x, plus key_ops and ext.
Every key a build of your app should carry, each with its kid, the id a lease names it by. In this order:
- The key that signs.
- The staged next key, once one is staged.
- Each previous key, until its retire date.
- For good, every key that has signed a license file or a key file, because such a file never expires and names the key that signed it.
POST /activate
Takes a seat on the key for this device and returns a lease. If a device with the same fingerprint already holds a seat on the key, it gets that seat back; no second seat is taken.
Request
Required. The license key, 1 to 120 characters. Case-insensitive; surrounding spaces are ignored.
Required. This device's identifier, 1 to 200 characters. The SDKs' built-in fingerprinters send a SHA-256 hash, never the raw machine ID. A custom fingerprinter's value is sent as it is.
machine or hostname: what the fingerprint hashes. Only machine holds the seat to its device.
The app's major version, a whole number. A 0.x app sends 1; the SDKs send 1 for a build set to 0 (see Tell the SDK each build's major).
The device's name for the license's device list, up to 120 characters.
The app's version, up to 40 characters, shown in the license's device list.
The operating system, up to 60 characters, shown in the license's device list.
The ids (kid) of the public keys this build carries, as a list. KeyGrant keeps up to 8 on the seat, so the dashboard's Signing key tab can count the devices whose builds carry a key before you switch to it.
- Optional, and never refused: a value that is not a list of key ids is ignored.
The email the key was sold to, up to 254 characters, as the person typed it. Read only when the key is locked to its buyer (Who can activate) and this device is taking a new seat; any other key ignores it.
- Compared without regard to case and surrounding spaces.
- Never written to the API logs.
The six-digit code KeyGrant emailed to the key's email, for a key locked to its buyer with a code. Single use, good for 10 minutes. Never written to the API logs.
Response
{ "lease": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrZXlncmFudCIs…", "activationId": "act_0f3c2a9d8e7b4c1a9f0e2d3c4b5a6978", "inGrace": false, "validUntil": null} The signed lease, a JWT. See Lease format.
The seat this device holds, act_ and 32 hex characters.
true when the key is past its expiry but inside its grace period.
When the key stops working (its expiry plus its grace), or null when it never does.
Errors
No such key for this product.
The key is revoked.
The key is past its expiry and grace.
The app's major is newer than the key covers.
The app's major is older than the key covers.
The product is archived. Only a device that already holds a seat may activate again.
Every seat on the key is taken.
Errors of a key locked to its buyer
Asked only of a key locked to its buyer, and only of a device taking a new seat. See Who can activate.
The key is locked to its buyer, this device is new, and no email was sent.
The email is not the one the key was sold to. The answer never says or hints at the right one.
The email was right and KeyGrant emailed a code to it. Send it as code.
The code is wrong, expired or already used. Also the answer to every code on a key once five wrong ones were sent in the hour.
Three codes were already emailed for the key this hour. The answer carries retryAfter in seconds.
Ten wrong emails were sent for the key this hour, or its codes are void after five wrong ones. The answer carries retryAfter in seconds.
The code could not be emailed, so nothing was activated. Send the request again.
Notes
-
A key on Version sold or Sold and later that was sold without a
version takes its version from this request's
majorat its first successful activation. ItsversionRuleisnullfrom then on. See The version a sale is for.
POST /validate
Renews the lease of a device that already holds a seat, with the key's current terms. It never takes a seat, and it keeps working after the product is archived.
Request
Required. The license key.
The seat to renew, up to 80 characters. Send this or fingerprint; if you send both, activationId is used.
The fingerprint the seat was activated with, used when no activationId is sent.
The app's major version.
The newest major the app ran under a lease signed during upgrade protection. See Upgrade protection.
Up to 4 fingerprints that identify this device, its own first. If the seat is held to a machine and none of them matches the seat, the response is 403 device.
What fingerprints[0] hashes: machine or hostname.
The app's version, the operating system and the public key ids this build carries, with the same limits as on activate.
Response
The same shape as activate's: lease, activationId, inGrace and validUntil.
Errors
No such key for this product.
The seat belongs to another machine. A copied license file gets this error even when the key is also revoked.
The key is revoked, or past its expiry and grace.
The app's major is newer, or older, than the key covers.
No seat on the key matches the activationId or fingerprint, for example because it was released.
POST /use
Spends uses of a meter, for a key whose license model counts them (see
Metered uses). It is what an SDK's use() calls.
Request
Required. The license key.
Required. The seat this device holds, as activate returned it. A device that holds no seat spends nothing.
Required. The meter's code, as the product lists it.
How many uses to spend, a whole number. The spend is whole or none.
Required. This spend's ID: 16 to 64 base64url characters, made by the app for each spend and unique for the key.
- A request that names an ID already recorded answers as that spend did and writes nothing, so a retry never spends twice.
Spends this device made with no answer, to report: a list of { id, meter, n, at }, oldest first, at in Unix milliseconds.
- An entry marked
"void": truetakes back a spend the device then refused. - They are recorded whatever the key's state.
As on validate. The device is checked as a validate checks it.
Response
{ spent: true, meters, spends, at }.
metershas{ left, total, offline, resets? }for each meter the request touched.spendsnames the reports it recorded.atis KeyGrant's time in Unix milliseconds.
Errors
The meter has fewer uses left than n. The answer is { error, left, meters, spends, at }, and nothing was spent.
The key has no meter with that code.
The seat belongs to another machine, or was released. Nothing is recorded.
The key is revoked or past its expiry and grace, or the app's major is outside what it covers. An archived product's keys keep spending, as they keep validating.
The body did not match, or the caller is past its limit (see Rate limits). An SDK reads both as no answer and spends from the device's offline allowance.
Notes
- The device must hold a seat on the key.
- A spend is not a check-in: it signs no lease and writes nothing on the seat.
- A meter never decides whether the app is licensed: a meter that is used up refuses spends, and the license keeps validating.
POST /deactivate
Frees a seat. Seats on revoked or expired keys can be freed too.
Request
Required. The license key.
The seat to free: send either one.
Response
A valid request returns 200 { "ok": true } whether or not a seat was
found, so the response only confirms that KeyGrant received the request.
POST /trial
Starts or resumes this device's trial of the product: a device trial, with no license key, one per fingerprint. See Trials.
Request
Required. This device's identifier, 1 to 200 characters.
machine or hostname.
The earliest trial start recorded on the device, in Unix milliseconds. A trial's start can only move earlier, and a start in the future is ignored.
The app's version, up to 40 characters.
Response
{ "ok": true, "trialStart": 1791257861000, "trialDays": 14, "trialEndsAt": 1792467461000, "lease": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9…"} -
The trial lasts the product's Trial length as it stands at each call, counted from
trialStart. -
The
leaseruns to the trial's end and is omitted once the trial has ended; an ended trial still returns200.
Errors
The device has no trial yet, and the product's Trial length is 0.
The device has no trial yet, and the product is archived.
POST /recover
Emails a customer the keys they have for the product.
Request
{ "email" }.
Response
A valid request returns 200 { "ok": true } whether or not the address
has any keys.
Errors
Past the endpoint's limit for one IP address (or IPv6 /64) on the product (see Rate limits).
That count cannot be made, so nothing is sent.
Notes
- The email lists the product's keys issued to that address that are not revoked, excluding test licenses.
-
The match ignores the case of the letters A to Z, so
[email protected]finds a key issued to[email protected], and the email goes to the address as sent. - It is sent only when the product's License delivery tab is set to KeyGrant sends them (see Key delivery email).
-
Past a limit on emails (per product and address, per address, and with the other emails a customer can
set off), the request still returns
200and no email is sent. See Emails a customer can set off.
POST /pickup
Emails a six-digit code to the address, the first step of key pickup. Key pickup restores a customer's license in an app that may not take a typed key, such as an App Store build: the customer proves the email they bought with by that code, and the app gets that email's keys.
-
Turn on Key pickup on the product's Settings tab first; while it is
off, both endpoints return
403 pickup-disabled. - An SDK's
restoreByEmail()makes both calls for you.
Request
{ "email" }, up to 254 characters.
Response
A valid request returns 200 { "ok": true } whether or not the address
has any keys, so the answer never says who your customers are.
Errors
The email is not an email address.
The product's Key pickup is off.
Past the endpoint's limit for one IP address (or IPv6 /64) on the product (see Rate limits).
That count cannot be made, so nothing is sent.
Notes
- The code is emailed only when the address has a live key of the product, and to the address on that key, as it was sold. The match ignores case and surrounding spaces, and test licenses never count.
- A code works once, for 10 minutes, and takes five wrong tries. Only the newest one works; if a newer one cannot be emailed, the one before it stays good.
- It is sent only when the product's License delivery tab is set to KeyGrant sends them, from the product's own sending address and branding.
-
Past the address's limit on codes, or your account's hourly limit on codes, the request still returns
200and nothing is sent. See Emails a customer can set off.
POST /pickup/confirm
Returns the address's keys of the product for the code. A successful answer uses the code up.
Request
{ "email", "code", "fingerprints" }:
Required. The same email the code was sent to.
Required. The code.
Optional. Up to 4 fingerprints of this device, as on validate.
Response
{ "keys": [ { "key": "UV-7KQ2-9XWM-4HTR-PL3D", "model": "perpetual", "modelName": "Household", "state": "active", "entitlements": {}, "expiresAt": null, "versions": { "minMajor": 1, "maxMajor": 1, "upgradeUntil": null }, "deviceLimit": 1, "activeDevices": 0, "held": false, "sale": { "id": "sale_4f1c", "index": 1, "size": 6 }, "userLock": "off" } ]} The license key, for your app to activate. Never show it: the point of a pickup is that the customer never types or sees one.
The key's type (perpetual, subscription, trial or timed) and the license model it was sold under, or null. Show modelName when the customer picks among several keys.
active or trial. Revoked keys, keys past their expiry and grace, test licenses and license files are never handed out.
What the key grants, and when it ends (Unix milliseconds, or null).
{ minMajor, maxMajor, upgradeUntil } as on the license object, or null for a key sold without a version that no app has activated yet: it covers whichever app activates it first.
The key's seats, and how many are taken.
true when one of the fingerprints sent holds a seat on the key: activate this one on a reinstall, so the device takes its seat back.
{ id, index, size } for a key that is one of a sale of several. Absent otherwise.
Who can activate the key. For a key locked to its buyer with a code, activationCode is a code this pickup has already proven: send it as code, with the email, on activate, and no second email is sent.
Errors
The product's Key pickup is off. Both endpoints answer this.
The email has a live code and this is not it.
The email has no live code: it expired, was used, was voided by its fifth wrong try, or was never asked for. A new code works at once.
Thirty wrong codes were sent for the email this hour, across every code it was sent. Every code is refused until the hour is out, the right one included. The answer carries retryAfter in seconds.
Past the endpoint's limit for one IP address (or IPv6 /64) on the product (see Rate limits).
The count of wrong codes could not be made, so the code was not checked. The answer carries retryAfter.
The count of requests from the IP address could not be made, so nothing was checked. The answer carries retryAfter.
Notes
-
A pickup takes no seat. Your app then activates one of the keys with
/activate, sending the email asemail(andactivationCodeascodewhen the key has one). The device limit, versions and the rest apply as to any key. - Each key handed out gets a line in its history and a
license.picked_upwebhook. - Neither the email, the code nor the fingerprints are written to the API logs.