Managing licenses from your server

If you bill outside Stripe, through your own checkout, invoices or a reseller, keep KeyGrant in sync from your backend: issue a license on a sale, extend it on a renewal, and revoke it on a refund. Every request, field and error is in Server endpoints.

… stands for /v1/licenses/:key.

WhenCall
A salePOST /v1/licenses
Find a customer's keysGET /v1/licenses
A buyer lands on your page after a Stripe or Paddle checkoutGET /v1/checkouts/:session/license
A renewalPOST …/extend
A refundPOST …/revoke
Free a deviceDELETE …/activations/:id
More or fewer devicesPOST …/devices
Other versionsPOST …/versions
A release of your appPOST /v1/products/:product/releases

Authenticate

  • Create a secret key on the dashboard's API keys page and send it as Authorization: Bearer <secret>.
  • A kg_test_ key issues test licenses and can access only those (see Testing your integration).
  • Each secret key has its own request limits (see Rate limits).

Warning

Keep the secret key on your server and never ship it in your app. A kg_live_ key can access every license in your account.

Issue on a sale

Call POST /v1/licenses with the product's API ID and the modelId of the license model that was bought. The model sets the expiry, device limit, grace period, Offline days, entitlements, versions and price.

Terminal
curl -X POST https://api.keygrant.dev/v1/licenses \  -H "Authorization: Bearer $KEYGRANT_SECRET" \  -H "content-type: application/json" \  -H "Idempotency-Key: $ORDER_ID" \  -d '{"product":"my-product","modelId":"mod_…","email":"[email protected]","version":"2.4.1","sendEmail":true}'
201 Created
{ "key": "MYPROD-9K4T-2M8P-XQ7V-H3DW", "emailed": true }

The Issue license form's Do this over the API panel shows the request with your real IDs. The options you will use most:

count

Issue that many keys as one sale, each with the same terms. A request issues one key otherwise, whatever the model's Keys per sale.

  • sendEmail mails them together in one email.
  • The response lists them all as keys, first to last.

Range 1 to 100

Idempotency-Key

Your order ID, as this header or as idempotencyKey in the body. A retry with the same idempotency key returns the original license key with a 200 instead of creating a second one.

  • A retry must name the same product and count, with a secret key of the same mode (live or test), or it returns 409 idempotency-key-reused. Its other fields are not compared.
version

The version being sold, for a model on Version sold or Sold and later. Without it, the key's first activation sets the version.

minMajor, maxMajor, upgradeMonths

To give one key its own versions, send minMajor and maxMajor together, and upgradeMonths for its upgrade protection.

sendEmail

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

emailed

In the response: whether this request sent the email. A retry that returns the original key sends none, so its emailed is false.

Look up licenses

Terminal
# One license, by its keycurl https://api.keygrant.dev/v1/licenses/MYPROD-9K4T-2M8P-XQ7V-H3DW \  -H "Authorization: Bearer $KEYGRANT_SECRET"
# What one customer holds, newest firstcurl "https://api.keygrant.dev/v1/[email protected]&status=active&limit=25" \ -H "Authorization: Bearer $KEYGRANT_SECRET"
  • The first request returns { license }.
  • The second returns { licenses, nextCursor }.
  • The email filter takes the full address and ignores case; partial addresses do not match.

A license is described in The license object.

After a checkout

To show the buyer their key on the page Stripe sends them to:

  1. Point the Payment Link's success URL at your page with ?session_id={CHECKOUT_SESSION_ID}.
  2. Request the license from your server with that ID.
  3. Retry for a few seconds on 404 unknown-checkout.

For a Paddle checkout, request it with the transaction ID (txn_…) that Paddle.js gives on checkout.completed, in the same call (see Selling through Paddle).

Terminal
curl https://api.keygrant.dev/v1/checkouts/cs_test_a1b2c3/license \  -H "Authorization: Bearer $KEYGRANT_SECRET"
200

{ license } once the key exists. A checkout that created several keys also returns every one of them, first to last, as licenses.

404 unknown-checkout

Not yet. The payment event can reach KeyGrant a moment after the buyer reaches your page, so retry for a few seconds.

404 checkout-held

KeyGrant held the sale: no key is coming, since you apply it to the key it was for or refund it. Tell the buyer you will be in touch (see Held sales).

Other 404 reasons

An upgrade checkout returns the key it changed only when the checkout named that key. Otherwise it answers 404 with a reason of its own (see Server endpoints, checkout), on which a page that polls may stop.

  • Check that license.product is the product your page sells.
  • Use a kg_live_ secret key, since a test secret key can access only test checkouts.

Extend on a renewal, revoke on a refund

Terminal
# A renewal: move the expiry to the end of the new period (Unix milliseconds)curl -X POST https://api.keygrant.dev/v1/licenses/MYPROD-9K4T-2M8P-XQ7V-H3DW/extend \  -H "Authorization: Bearer $KEYGRANT_SECRET" \  -H "content-type: application/json" \  -d '{"expiresAt":1822435200000}'
# A refundcurl -X POST https://api.keygrant.dev/v1/licenses/MYPROD-9K4T-2M8P-XQ7V-H3DW/revoke \ -H "Authorization: Bearer $KEYGRANT_SECRET" \ -H "content-type: application/json" \ -d '{"reason":"refund"}'
  • extend takes the new expiry as an absolute time, so a retried call lands on the same expiry instead of adding the period twice.
  • extend never shortens a license, and it cannot bring back a revoked one.
  • revoke takes a reason of refund, chargeback, subscription_canceled or manual (the default). The key stops working at the app's next check-in.

Report a renewal before the key ends. Up to a day after a subscription key's grace period ends or a trial key expires, KeyGrant revokes it.

Free a device

DELETE /v1/licenses/:key/activations/:id frees one device's seat. To find the activation ID:

Change a key's device limit

Terminal
# The customer bought two more devices: 3 becomes 5curl -X POST https://api.keygrant.dev/v1/licenses/MYPROD-9K4T-2M8P-XQ7V-H3DW/devices \  -H "Authorization: Bearer $KEYGRANT_SECRET" \  -H "content-type: application/json" \  -d '{"deviceLimit":5}'
  • The key gets that limit in place of its model's; the model and its other keys keep theirs.
  • Lowering it releases no device: the ones already active keep working, and new ones are refused until fewer than the limit are active.

See Change one key's limit.

Change a key's versions

Terminal
# Let a 1.x key run 2.x and every later version toocurl -X POST https://api.keygrant.dev/v1/licenses/MYPROD-9K4T-2M8P-XQ7V-H3DW/versions \  -H "Authorization: Bearer $KEYGRANT_SECRET" \  -H "content-type: application/json" \  -d '{"minMajor":1,"maxMajor":null}'

Send both bounds every time; maxMajor null means "and later". The app picks up the change at its next check-in.

Report a release

Terminal
# In your release script, with a live secret keycurl -X POST https://api.keygrant.dev/v1/products/my-product/releases \  -H "Authorization: Bearer $KEYGRANT_SECRET" \  -H "content-type: application/json" \  -d '{"version":"3.1.4"}'# {"product":"my-product","currentMajor":3}

This call moves your product's current version up to the major you send.

  • It only moves up, never down, so call it on every release, including a hotfix on an older branch.
  • Call it before you publish the build, so that upgrade protection can keep the new version for the first customers who run it.
  • Test secret keys cannot call it.