License keys
Sell access to your software with license keys. Each member who purchases gets a unique key; your software validates it against Subicon’s API to unlock or lock access.
#How it works
- A member who purchases a product in your community automatically gets one license key for this app (shown under “Your license keys”).
- You configure the downloads (one URL per platform) and, optionally, let members regenerate their key.
- Your software sends the key to the validation endpoint and trusts its answer.
A member gets one key per purchased product that unlocks this app, so the same person may have several keys.
#Validation endpoint
A public, unauthenticated GET endpoint — the key itself is the credential. It is rate-limited per license + network.
GET https://subicon.io/api/software/validate?key=XXXX-XXXX-XXXX-XXXX&nonce=<unique-per-call>&device=<machine-id>#Request
Pass the license key as the “key” query parameter (dashes optional, case-insensitive). A unique `nonce` per call is REQUIRED — without it (or when reusing one) the endpoint always answers valid:false. Optionally add a stable `device` id to enforce device limits.
curl "https://subicon.io/api/software/validate?key=ABCD-EFGH-JKMN-PQRS&nonce=b3d1-unique-per-call&device=my-machine"#Response
JSON. The endpoint re-checks the member’s purchase live, so a refund or cancellation invalidates the key automatically.
{ "valid": true, "status": "active", "software": "Your community", "product": "Pro", "nonce": "n1", "ts": 1733520000, "signature": "..." }Security: every failure returns an identical response — valid:false, status:"invalid", with software, product and signature all null. Only a valid key returns the signed details (status "active"). Your software must NOT branch on a specific failure reason — treat anything other than valid:true as “locked”.
{ "valid": false, "status": "invalid", "software": null, "product": null, "nonce": "n1", "ts": 1733520000, "signature": null }| status | Meaning |
|---|---|
active | Valid key, purchase still active → unlock. |
invalid | Anything else — unknown/malformed key, refund, cancellation, expiry, revoked, or device limit reached. For privacy & anti-piracy, all of these return the SAME response, so the endpoint never reveals whether a key exists or why it failed → lock. |
#Devices & activation limits
Send a stable `device` id. If the seller set a per-product device limit, a new device beyond the limit gets the same uniform `invalid` response as any other failure (the endpoint never reveals why).
#Verifying the signature
Every response is signed (Ed25519) with your app’s private key. Verify it with your app’s public key (found in the Software app settings) to be sure the response really comes from Subicon.
Signed message = `valid|status|key|nonce|ts` (pipe-joined), where `key` is the normalized key (uppercase, dashed). The `nonce` is required and single-use per key: the server signs a given (key, nonce) pair only once, so a replayed response can never carry a fresh signature. Check the nonce you sent is echoed back.
#Integrating in your software
Subicon provides the key and this endpoint. It is up to your software to call it (at launch and/or periodically) and to allow or block access based on the response.
import { verify, createPublicKey } from 'crypto';
// The signed message uses the NORMALIZED key: uppercase, grouped XXXX-XXXX-XXXX-XXXX.
const normalizedKey = licenseKey.toUpperCase().replace(/[^0-9A-Z]/g, '').replace(/(.{4})(?=.)/g, '$1-');
const nonce = crypto.randomUUID(); // REQUIRED — unique on every call
const url = 'https://subicon.io/api/software/validate'
+ '?key=' + encodeURIComponent(normalizedKey)
+ '&device=' + encodeURIComponent(deviceId) // a stable per-machine id
+ '&nonce=' + nonce;
const data = await (await fetch(url)).json();
// Verify the signature with your app's PUBLIC KEY (from the Software app settings).
const msg = `${data.valid}|${data.status}|${normalizedKey}|${data.nonce}|${data.ts}`;
const ok = data.signature && verify(null, Buffer.from(msg), createPublicKey(PUBLIC_KEY_PEM), Buffer.from(data.signature, 'base64url'));
if (ok && data.valid) startApp();
else showInvalidKey(data.status);Tip: cache the result briefly and re-check on launch. Always verify the response signature (see “Verifying the signature”).