GuidesCheck licenses in your app
Check licenses
in your app.
Your app asks anyshop whether a buyer's key is good, and on which devices, without shipping an API key: the license key is the credential. Four public calls, JSON in and out, the same in test and live mode.
A license product gives each order its own key, with the device limit, expiry and updates period you set on the product. Your app sends that key, and an ID of the device it runs on, to these calls:
POST /v1/licenses/activatewhen the buyer enters the key: it takes a slot on this device, within the limit.POST /v1/licenses/validateon launch: is the key still good, and is it active on this device?POST /v1/licenses/deactivatewhen the buyer signs out on this device, which frees its slot.POST /v1/licenses/offlinefor a file your app can check with no network at all, for 30 days.
The device ID
Pick an identifier that stays the same on every launch and says nothing about the person: a random ID your app stores the first time it runs, or a hash of the machine's own ID. It is up to 200 characters, and anyshop keeps only its hash. name and platform are optional: the seller sees them next to the activation, so something like Studio Mac and macOS helps them free the right device when a buyer asks.
Activate on first run
Send the key the buyer typed, as they typed it: spaces and case are forgiven. Activating a device that is already active does not take a second slot, so it is safe to call again after a reinstall.
curl https://api.anyshop.io/v1/licenses/activate \
-H "Content-Type: application/json" \
-d '{
"key": "PXF-7Q2M-K8WD-4H9R-2LTN",
"device_id": "a3f9c2e17b8d4e05",
"name": "Studio Mac",
"platform": "macOS"
}'{
"ok": true,
"license": {
"id": "lic_01m3rv2f8k1q7w3e5r9t0y4u6i",
"status": "active",
"livemode": false,
"product": { "id": "prod_01m3rr5kmbeb2s18x1kpntjxqb", "name": "Pixelforge Studio" },
"expires_at": null,
"updates_until": "2027-10-02T14:21:07.000Z",
"devices": { "used": 1, "limit": 2 },
"activation": { "activated_at": "2026-10-02T14:21:07.000Z", "name": "Studio Mac" }
}
}Validate on launch
With device_id, validate also checks that the license is active on this device; without it, only that the key is good. Keep the last good answer, and check again on launch and every few hours while the app runs, not on every action: the rate limits below are generous for that and tight for anything more.
curl https://api.anyshop.io/v1/licenses/validate \
-H "Content-Type: application/json" \
-d '{ "key": "PXF-7Q2M-K8WD-4H9R-2LTN", "device_id": "a3f9c2e17b8d4e05" }'updates_until is when the buyer's updates period ends: compare it with your release date to decide whether this version is covered. expires_at is set only for licenses that end; after it, validate answers expired.
Sign out on this device
Deactivate frees the device's slot, so the buyer can move the license to a new computer without asking you. The seller can also free a slot from the dashboard, or with DELETE /v1/licenses/{id}/activations/{activation} from your server.
curl https://api.anyshop.io/v1/licenses/deactivate \
-H "Content-Type: application/json" \
-d '{ "key": "PXF-7Q2M-K8WD-4H9R-2LTN", "device_id": "a3f9c2e17b8d4e05" }'When the answer is no
Every refusal has the same body, { "ok": false, "error": { "code", "message" } }. Branch on code; message is written for a person, in English.
| Status | code | What your app does |
|---|---|---|
| 400 | invalid_key | Not a license key, likely a typo. Ask again. |
| 400 | device_required | Activate and deactivate need a device_id. |
| 404 | not_found | No license has this key. Ask again, or send the buyer to their order page. |
| 404 | not_activated | The key is good, but not active on this device. Activate it. |
| 403 | revoked | The seller revoked it, after a refund for example. Stop and explain. |
| 403 | expired | Past expires_at. Offer a renewal. |
| 409 | device_limit | Active on as many devices as it allows. Ask the buyer to sign out on another one. |
| 429 | rate_limited | Too many calls. Wait for the seconds in Retry-After, then try again. |
Rate limits
300 calls a minute from one network, and 60 a minute for one license, across all four calls. An office behind one address shares the first budget, so spread checks out instead of running them all at the top of the hour.
Work offline
POST /v1/licenses/offline returns a license file signed by your store's Ed25519 key. Save it next to your app's settings. Your app verifies it with the store's public key, which you build into the app: find it in the dashboard under Developers, Docs. Test and live licenses are signed with different keys.
{
"ok": true,
"file": {
"format": "anyshop-license",
"version": 1,
"alg": "Ed25519",
"payload": "eyJsaWNlbnNlIjoibGljXzAxbTNydjJm...",
"signature": "q8Zc0v2mK3a..."
}
}The signature covers the payload text exactly as sent. The payload is base64url JSON with license, key, store, product, livemode, devices, expiresAt, updatesUntil, issuedAt and checkBy, 30 days after it was issued. Before checkBy, get a fresh file online.
import { createPublicKey, verify } from "node:crypto";
// Developers, Docs in the dashboard: the public key, as PEM, built into your app.
const publicKey = createPublicKey(STORE_PUBLIC_KEY_PEM);
export function readLicenseFile(file) {
const signature = Buffer.from(file.signature, "base64url");
if (file.alg !== "Ed25519") return null;
if (!verify(null, Buffer.from(file.payload), publicKey, signature)) return null;
const license = JSON.parse(Buffer.from(file.payload, "base64url").toString("utf8"));
return new Date(license.checkBy) > new Date() ? license : null;
}From your own server
To look licenses up or act on them, call the API with a key from your server, never from the app: GET /v1/licenses and GET /v1/licenses/{id} need licenses:read; POST /v1/licenses/{id}/revoke and freeing a device need licenses:write.