Licensing API.
Activate a license on a deployment, verify it from your server, and keep running through short outages.
scriptgo-sdk/docs/site/licensing-api.mdOn this page
Paid resources check their license at runtime. This page is for the people running those deployments (customers) and for creators who add a license check to their resource.
Who does what
Every purchase on SCRIPT//GO involves three parties, and every purchase screen names them:
- The creator sells. The creator sets the offers and is the seller.
- The provider takes the payment. For example Tebex. In development, a mock provider plays this role.
- SCRIPT//GO issues the license. The license, its key and its deployments live here.
A license is only granted by a signed provider webhook that SCRIPT//GO verified, or by an audited administrative grant. Returning from the checkout page grants nothing on its own. Checkout through Tebex stays disabled until Tebex approves SCRIPT//GO in writing. A seller's store needs no approval by SCRIPT//GO staff.
The flow
- Get a key. After payment, the license appears in Dashboard → Licenses
with a key such as
sg_live_…a91c. You can copy and rotate it there. - Activate each deployment once. Activation binds the license to a named instance
(
prod-eu-1) in one environment and returns adeploymentToken. - Verify regularly from the server. The answer includes a short-lived token signed with ed25519.
- Offline grace. If SCRIPT//GO is unreachable, the deployment may keep running until the
offlineGraceUntilof its last valid answer. The period is set by the platform (LICENSE_OFFLINE_GRACE_HOURS, 72 hours by default).ScriptGo.licensingdoes not track it for you: the resource keeps its last valid answer, for example inScriptGo.storage.
You can also add a deployment from the license page in the dashboard. It calls
POST /v1/me/licenses/:id/deployments and also returns a deploymentToken.
Warning
Keep the license key on your server. Never ship it inside client-side scripts, a game build or a public repository. Anyone holding the key can use your deployment slots.
Endpoints
All bodies are JSON. Errors use the standard body
{ "error": { "code": "snake_case", "message": "Human sentence." } }.
POST /v1/licenses/activate
{
"licenseKey": "sg_live_…",
"package": "kestrel//inventory-pro",
"environment": "luanti",
"deploymentName": "prod-eu-1",
"adapterVersion": "0.3.0"
}Response, HTTP 201:
{ "activationId": "…", "deploymentToken": "…" }package is the creator//name identifier or the manifest resource name (inventory_pro).
Activating the same deployment name again in the same environment rotates its token.
Activation fails with:
| HTTP | Code | Meaning |
|---|---|---|
| 404 | unknown_key |
The key does not exist, or belongs to another package |
| 409 | deployment_limit |
Every deployment slot is in use. Deactivate an old deployment first |
| 409 | deployment_exists |
A deployment with this name exists in another environment |
| 409 | the license status, e.g. expired |
The license cannot be used |
| 422 | environment_not_covered |
The license does not cover this environment |
POST /v1/licenses/verify
Send exactly one of the license key or the deployment token:
{
"deploymentToken": "…",
"package": "kestrel//inventory-pro",
"environment": "luanti",
"adapterVersion": "0.3.0"
}Response:
{
"valid": true,
"reason": null,
"status": "active",
"activationStatus": "active",
"expiresAt": "…",
"offlineGraceUntil": "…",
"environments": ["standalone", "love2d", "godot", "luanti"],
"token": "…"
}token is ed25519-signed and short-lived. Store it on the deployment; it is what proves the
license during an outage.
When the license cannot be used, the answer is still HTTP 200: valid is false and a reason
says why. Any 4xx or 5xx answer, 429 included, means the API could not answer; keep relying on
the last valid answer. Only a body with neither or both of licenseKey and deploymentToken
gets a 400.
| Reason | Meaning | What to do |
|---|---|---|
expired |
The paid period ended | Renew at the provider, then verify again |
revoked |
The license was revoked | Read the reason on the license page; contact support |
refunded |
The payment was refunded | Buy a new license |
deactivated |
The deployment token belongs to a deactivated deployment | Activate the deployment again |
environment_not_covered |
The plan does not cover this environment | Use a covered environment or change plan |
deployment_limit |
More deployments are active than the license allows | Deactivate an unused deployment |
unknown_key |
The key or token does not exist, is malformed, or belongs to another package | Check for a typo, or a key rotated since |
verify never reveals another customer's data, and answers the same way to every unknown key.
POST /v1/licenses/deactivate
{ "licenseKey": "sg_live_…", "activationId": "…" }Response: { "ok": true }. The slot is free again.
POST /v1/licenses/key
For a protected release. It answers the release's content key, encrypted to one server.
{
"deploymentToken": "sg_dep_…",
"package": "kestrel//inventory-pro",
"version": "1.5.0",
"environment": "dotnet",
"serverPublicKey": "…"
}serverPublicKey is the server's X25519 public key, raw, in base64url. The first request binds it
to the deployment. After that, a request with another key is refused: a copy of the files on
another machine has no matching private key and gets nothing. version is optional; without it
the newest protected release answers.
{
"valid": true,
"reason": null,
"wrappedKey": { "epk": "…", "nonce": "…", "wrapped": "…" },
"aad": "kestrel//inventory-pro@1.5.0",
"signerPublicKey": "…",
"deliveryWatermark": "…",
"offlineGraceUntil": "2026-11-13T10:00:00.000Z",
"expiresAt": "2026-11-10T10:15:00.000Z"
}The loader unwraps the key with its private key, checks the bundle against signerPublicKey,
decrypts, and stamps deliveryWatermark into the copy. It asks again before expiresAt and may
keep running until offlineGraceUntil when the API cannot answer. Like verify, a definitive
refusal is HTTP 200 with valid: false:
reason |
Meaning |
|---|---|
unknown_key |
No licence matches the key or token for this package |
expired, revoked, refunded |
The licence ended. A running server stops when its offline grace ends |
deactivated |
The deployment was deactivated |
environment_not_covered, deployment_limit |
As for verify |
unbound |
The request named no server key (a licence key alone, or no serverPublicKey on a deployment that has none) |
server_key_mismatch |
Another server key than the bound one. After moving or re-installing a server, its owner uses Reset server key on the licence page, then starts it again |
not_escrowed |
The package has no protected release for that version |
Every answer is logged with its reason. The key itself is never logged or cached. The licence
owner resets a binding with POST /v1/me/licenses/:id/deployments/:activationId/reset-server-key
(recorded, three per deployment per day).
GET /v1/signing-keys
{ "keys": [{ "keyId": "…", "alg": "ed25519", "publicKey": "…", "status": "current" }] }The registry's release-signing keys. Every published release is signed with the current one;
scriptgo add checks it. This is a different key from the one below. A creator's own escrow keys
are at GET /v1/creators/:username/signing-keys.
GET /v1/licenses/public-key
{ "alg": "ed25519", "publicKey": "…", "kid": "…" }Use this key to check the signature of verify tokens offline.
GET /v1/licenses/:id
Returns the full license (deployments, history). Owner only, with a signed-in session or a personal access token, so it is meant for the dashboard, not for deployments.
Rate limits
verify and key are rate-limited per key (60 per minute) and per IP (300 per minute); activate and
deactivate allow 30 per minute per IP. Verify on start and then at a modest interval, not on
every tick or every player action. When a call is refused, keep using the last signed token and
retry later.
curl
curl -sS -X POST "$SCRIPTGO_API/v1/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$SCRIPTGO_LICENSE_KEY"'","package":"kestrel//inventory-pro","environment":"luanti","deploymentName":"prod-eu-1","adapterVersion":"0.3.0"}'
curl -sS -X POST "$SCRIPTGO_API/v1/licenses/verify" \
-H "Content-Type: application/json" \
-d '{"deploymentToken":"'"$SCRIPTGO_DEPLOYMENT_TOKEN"'","package":"kestrel//inventory-pro","environment":"luanti","adapterVersion":"0.3.0"}'Read the key and token from environment variables or a secrets store, as above, never from a file inside the resource.
From Lua
Creators add a license check to their resource with the licensing capability.
-- scriptgo.lua
licensed true
capabilities { "events", "storage", "licensing" }-- server/license.lua
CreateThread(function()
local result, code = ScriptGo.licensing.verify({
licenseKey = GetConvar("inventory_pro_license_key", ""),
package = "kestrel//inventory-pro",
})
if not result then
-- Transport or host error: nil, code, body (see Runtime).
ScriptGo.log.error("Inventory Pro license check could not run: " .. tostring(code))
return
end
if not result.valid then
ScriptGo.log.error("Inventory Pro license check failed: " .. tostring(result.reason))
return
end
ScriptGo.log.info("Inventory Pro licensed until " .. tostring(result.expiresAt))
end)licensing.verify(opts) is server side only, runs inside a thread, and calls
POST /v1/licenses/verify. opts takes licenseKey or deploymentToken, and package; the
host adds environment and adapterVersion itself. licensing.activate(opts) takes
licenseKey, package and deploymentName and calls POST /v1/licenses/activate. Both send to
the API named by the scriptgo_api convar (default https://api.scriptgo.dev) and return the
decoded answer, or nil, code, body when the call failed or the API answered 4xx/5xx. Set
scriptgo_api only to test against another API, for example a local one
(http://localhost:4100).
Requires capability licensing
A resource that declares licensing as required does not start where the host cannot provide
it. Licensing goes through the host's outbound HTTP: Godot has none, Luanti needs the mod in
secure.http_mods, and OPEN//77 needs the operator to set http.enabled and list the API host
in http.allowedHosts in server.jsonc.
License states
| Status | Meaning |
|---|---|
active |
Paid and current |
grace |
A payment failed; still active until a date |
cancelled_active |
Cancelled at the provider; active until the end of the paid period |
expired |
The period ended; deployments are blocked |
revoked |
Revoked by SCRIPT//GO, with a reason |
refunded |
Refunded through the provider |
Deployments report pending (waiting for the first check), active, offline_grace,
deactivated or blocked.

