Check license keys from your app in Node or Go: device limits, expiry and revocation
When Skalp generates a license key for every sale, your app can check it with one API call. The answer says whether the key works and, if not, why: expired, revoked after a refund, over its device limit, or tied to a subscription that ended. Below is the setup and the code for Node, Go and plain HTTP.
Set up the product
- Create a product and set its delivery to License key. Every order then gets a fresh key.
- Set a device limit if one key should only run on a few machines, and a duration if the license should expire after a number of days. Leave the duration empty for a lifetime license, or sell the product as a subscription so the key works while the plan is paid.
- Create an API key with only the
licenses:validate scope. With that scope alone the key cannot read orders or change anything, so it can ship inside your app.
Node
import { Skalp } from '@skalpgg/sdk'
const skalp = new Skalp({ apiKey: LICENSE_CHECK_KEY })
const result = await skalp.licenses.validate({
key: enteredKey,
hardwareId: machineId,
})
if (!result.valid) {
showError(result.message)
} else {
unlock(result.license?.expiresAt)
}
The package runs on Node 20+, Bun, Deno and edge runtimes. It refuses to start inside a browser with an API key unless you pass dangerouslyAllowBrowser: true, so a check in a web page belongs on your server.
Go
client := skalp.New(skalp.Options{APIKey: licenseCheckKey})
res, err := client.ValidateLicense(ctx, skalp.ValidateLicenseParams{
Key: enteredKey,
HardwareID: machineID,
})
if err != nil {
return err
}
if !res.Valid {
return fmt.Errorf("license: %s", res.Message)
}
Any other language
curl https://api.skalp.gg/v1/licenses/validate \
-H "x-api-key: $SKALP_LICENSE_CHECK_KEY" \
-H "content-type: application/json" \
-d '{"key":"THE-BUYERS-KEY","hardwareId":"9f2c41..."}'
Add "productId" to the body if a key from one of your other products must not unlock this one.
What the codes mean
| Code | Meaning |
VALID | The key works. The response also carries its expiry date and how many devices it uses. |
NOT_FOUND | No key like this exists in your store. |
PRODUCT_MISMATCH | The key belongs to a different product than the productId you sent. |
LICENSING_DISABLED | The product's delivery is not set to License key. |
REVOKED | The order was refunded or lost a chargeback. |
SUSPENDED | The license has been suspended. |
EXPIRED | The license has passed its end date. |
SUBSCRIPTION_INACTIVE | The subscription behind the key has ended. |
DEVICE_LIMIT_REACHED | The key is already bound to as many machines as the product allows. |
Device limits
If you send a hardwareId and the product has a device limit, the first check from a new machine binds that machine to the key. Checks from bound machines pass, and a new machine beyond the limit gets DEVICE_LIMIT_REACHED. Send a value that stays the same on one machine, and hash it first so no raw hardware details leave the computer. When a buyer replaces their PC, your server can free the slots with licenses.resetDevices. That call needs the licenses:write scope, so it never belongs in the app itself.
Working offline
Check the key when the app starts and store the last good answer with a timestamp. If there is no network, accept the stored answer for a grace period you choose, a few days for example, and check again when the app is back online. A key revoked in the meantime fails at that check.
Keeping your own copy
To check keys in your own backend instead, take them from the order.paid webhook, which carries the licenses of the order. The refund and dispute webhooks tell you when to switch a key off. Webhooks and the API.
Questions people ask
Is it safe to put an API key inside a desktop app?
Only a key whose single scope is licenses:validate. It can check keys and do nothing else.
Can a buyer move their license to a new computer?
Yes, once you free their devices with licenses.resetDevices from your server or your support tool.
What happens to the key when a subscription ends?
From then on the check returns SUBSCRIPTION_INACTIVE.
Do I have to use the SDK?
No. The check is one HTTPS POST with a JSON body, so any language that can send a request works.