anyshop docs homeStatusOpen the dashboard

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.

  • 4 public calls
  • No API key in your app
  • Offline for 30 days

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/activate when the buyer enters the key: it takes a slot on this device, within the limit.
  • POST /v1/licenses/validate on launch: is the key still good, and is it active on this device?
  • POST /v1/licenses/deactivate when the buyer signs out on this device, which frees its slot.
  • POST /v1/licenses/offline for 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"
  }'
200Response
{
  "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
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.

StatuscodeWhat your app does
400invalid_keyNot a license key, likely a typo. Ask again.
400device_requiredActivate and deactivate need a device_id.
404not_foundNo license has this key. Ask again, or send the buyer to their order page.
404not_activatedThe key is good, but not active on this device. Activate it.
403revokedThe seller revoked it, after a refund for example. Stop and explain.
403expiredPast expires_at. Offer a renewal.
409device_limitActive on as many devices as it allows. Ask the buyer to sign out on another one.
429rate_limitedToo 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.

200 Response
{
  "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.

verify.js
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.