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.

EndpointDoes
GET /pubkeyReturns the public keys that verify leases.
POST /activateTakes a seat for this device and returns a lease.
POST /validateRenews the lease of a device that holds a seat.
POST /useSpends uses of a meter.
POST /deactivateFrees a seat.
POST /trialStarts or resumes this device's trial.
POST /recoverEmails a customer their keys.
POST /pickupEmails a code to restore a license by email.
POST /pickup/confirmReturns the email's keys for the code.
  • Every path is under /v1/products/:slug/, where :slug is 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 }:

publicJwk

The key that signs leases now, as a JWK with kty OKP, crv Ed25519, alg EdDSA and x, plus key_ops and ext.

keys

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

key

Required. The license key, 1 to 120 characters. Case-insensitive; surrounding spaces are ignored.

fingerprint

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.

fingerprintKind

machine or hostname: what the fingerprint hashes. Only machine holds the seat to its device.

major

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).

Range 1 to 99,999 ; Default 1

name

The device's name for the license's device list, up to 120 characters.

version

The app's version, up to 40 characters, shown in the license's device list.

platform

The operating system, up to 60 characters, shown in the license's device list.

kids

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.
email

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.
code

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

200
{  "lease": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrZXlncmFudCIs…",  "activationId": "act_0f3c2a9d8e7b4c1a9f0e2d3c4b5a6978",  "inGrace": false,  "validUntil": null}
lease

The signed lease, a JWT. See Lease format.

activationId

The seat this device holds, act_ and 32 hex characters.

inGrace

true when the key is past its expiry but inside its grace period.

validUntil

When the key stops working (its expiry plus its grace), or null when it never does.

Errors

404 invalid-key

No such key for this product.

403 revoked

The key is revoked.

403 expired

The key is past its expiry and grace.

403 upgrade-required

The app's major is newer than the key covers.

403 update-required

The app's major is older than the key covers.

409 product-disabled

The product is archived. Only a device that already holds a seat may activate again.

409 limit

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.

403 user-email-required

The key is locked to its buyer, this device is new, and no email was sent.

403 user-email-mismatch

The email is not the one the key was sold to. The answer never says or hints at the right one.

403 user-code-required

The email was right and KeyGrant emailed a code to it. Send it as code.

403 user-code-invalid

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.

429 too-many-codes

Three codes were already emailed for the key this hour. The answer carries retryAfter in seconds.

429 too-many-attempts

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.

503 try-again

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 major at its first successful activation. Its versionRule is null from 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

key

Required. The license key.

activationId

The seat to renew, up to 80 characters. Send this or fingerprint; if you send both, activationId is used.

fingerprint

The fingerprint the seat was activated with, used when no activationId is sent.

major

The app's major version.

Range 1 to 99,999 ; Default 1

keptMajor

The newest major the app ran under a lease signed during upgrade protection. See Upgrade protection.

fingerprints

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.

fingerprintKind

What fingerprints[0] hashes: machine or hostname.

version, platform, kids

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

404 invalid-key

No such key for this product.

403 device

The seat belongs to another machine. A copied license file gets this error even when the key is also revoked.

403 revoked, 403 expired

The key is revoked, or past its expiry and grace.

403 upgrade-required, 403 update-required

The app's major is newer, or older, than the key covers.

403 deactivated

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

key

Required. The license key.

activationId

Required. The seat this device holds, as activate returned it. A device that holds no seat spends nothing.

meter

Required. The meter's code, as the product lists it.

n

How many uses to spend, a whole number. The spend is whole or none.

Range 1 to 1,000,000,000 ; Default 1

id

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

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": true takes back a spend the device then refused.
  • They are recorded whatever the key's state.
major, fingerprints, fingerprintKind, kids

As on validate. The device is checked as a validate checks it.

Response

200

{ spent: true, meters, spends, at }.

  • meters has { left, total, offline, resets? } for each meter the request touched.
  • spends names the reports it recorded.
  • at is KeyGrant's time in Unix milliseconds.

Errors

409 used-up

The meter has fewer uses left than n. The answer is { error, left, meters, spends, at }, and nothing was spent.

404 unknown-meter

The key has no meter with that code.

403 device, 403 deactivated

The seat belongs to another machine, or was released. Nothing is recorded.

403 revoked, 403 expired, 403 upgrade-required, 403 update-required

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.

400 invalid-request, 429 rate-limited

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

key

Required. The license key.

activationId, fingerprint

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

fingerprint

Required. This device's identifier, 1 to 200 characters.

fingerprintKind

machine or hostname.

clientStart

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.

version

The app's version, up to 40 characters.

Response

200
{  "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 lease runs to the trial's end and is omitted once the trial has ended; an ended trial still returns 200.

Errors

403 trials-disabled

The device has no trial yet, and the product's Trial length is 0.

409 product-disabled

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

429 rate-limited

Past the endpoint's limit for one IP address (or IPv6 /64) on the product (see Rate limits).

503 unavailable

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 200 and 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

400 invalid-request

The email is not an email address.

403 pickup-disabled

The product's Key pickup is off.

429 rate-limited

Past the endpoint's limit for one IP address (or IPv6 /64) on the product (see Rate limits).

503 unavailable

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 200 and 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" }:

email

Required. The same email the code was sent to.

code

Required. The code.

fingerprints

Optional. Up to 4 fingerprints of this device, as on validate.

Response

200
{  "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"    }  ]}
key

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.

model, modelName

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.

state

active or trial. Revoked keys, keys past their expiry and grace, test licenses and license files are never handed out.

entitlements, expiresAt

What the key grants, and when it ends (Unix milliseconds, or null).

versions

{ 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.

deviceLimit, activeDevices

The key's seats, and how many are taken.

held

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.

sale

{ id, index, size } for a key that is one of a sale of several. Absent otherwise.

userLock, activationCode

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

403 pickup-disabled

The product's Key pickup is off. Both endpoints answer this.

403 wrong-code

The email has a live code and this is not it.

403 code-expired

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.

429 too-many-attempts

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.

429 rate-limited

Past the endpoint's limit for one IP address (or IPv6 /64) on the product (see Rate limits).

503 try-again

The count of wrong codes could not be made, so the code was not checked. The answer carries retryAfter.

503 unavailable

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 as email (and activationCode as code when 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_up webhook.
  • Neither the email, the code nor the fingerprints are written to the API logs.