Licenses
A license is a key that unlocks software you sell. Fulkruma issues one license per fulfilment of a license-type product at checkout, tracks how many devices ("instances") have activated it, and exposes three public endpoints — activate, deactivate, validate — that the buyer's software calls directly with only the key.
Keys are formatted as 5 groups of 5 base32 characters, e.g. 7K3PH-9X2RM-4LBQE-PT86W-JF1RV. They're stable, never reissued, and revoking them transitions the key to revoked — the public endpoints then refuse further activation.
The merchant-facing endpoints (/licenses POST/GET, /licenses/:id/revoke) require signed requests; the public endpoints are unauthenticated. See Authentication for the signing recipe.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/api/v1/licenses |
HMAC | List licenses |
POST |
/api/v1/licenses |
HMAC | Issue a license |
POST |
/api/v1/licenses/:id/revoke |
HMAC | Revoke a license |
GET |
/api/v1/licenses/validate |
public | Check whether a key is still valid |
POST |
/api/v1/licenses/activate |
public | Bind a key to an instance |
POST |
/api/v1/licenses/deactivate |
public | Release an instance |
List licenses
GET /api/v1/licenses
Returns up to 100 licenses in the workspace, newest first.
Response shape
{
"data": {
"licenses": [
{
"id": "lic_01HXAB7K3M9N2P5QRS8TVWXY3Z",
"accountId": "acc_01HX...",
"productId": "prod_01HX...",
"customerId": "cus_01HX...",
"key": "7K3PH-9X2RM-4LBQE-PT86W-JF1RV",
"status": "active",
"activations": 1,
"maxActivations": 3,
"expiresAt": null,
"externalSource": "storlaunch",
"externalRef": "ord_19238",
"createdAt": "2026-05-12T10:42:00.123Z"
}
]
},
...
}
Issue a license
POST /api/v1/licenses
Issues a new license for a product + customer pair and emits a fulkruma.license.issued.v1 event. The Plugipay webhook handler calls this automatically when a checkout for a license-type product completes; you can also call it manually for off-platform sales or upgrades.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
productId |
string (prod_…) |
yes | Must reference a license-type product in the workspace. |
customerId |
string | yes | The buyer's ID. Free-form (Fulkruma doesn't enforce a particular format; some merchants store Plugipay customer IDs here, others their own). |
maxActivations |
integer (≥1) | no | Default 1. The number of distinct instance IDs that can activate this key concurrently. |
expiresAt |
string (ISO 8601) | no | Hard expiry. After this, validate returns valid: false with status: "expired". |
externalSource |
string (≤50) | no | Where the issue originated (storlaunch, your storefront name). |
externalRef |
string (≤255) | no | The originating system's order/sale ID. |
Response — 201 Created. The full license object with the generated key field. The plaintext key is also stored on Fulkruma's row — you can re-fetch via list/retrieve.
Email the key to the buyer in the same request. Fulkruma doesn't send the email itself in v1 — you wire that up on your side. Subscribe to
fulkruma.license.issued.v1if your issuance pipeline is async.
Revoke a license
POST /api/v1/licenses/:id/revoke
Marks the license revoked and emits a fulkruma.license.revoked.v1 event. The public activate endpoint then returns 404 INVALID_KEY on further attempts; validate returns valid: false. Existing active instances are not forcibly cut off — they keep working until the next time they call validate. There is no unrevoke; mint a new license.
Errors
| Status | error.code |
When |
|---|---|---|
404 |
NOT_FOUND |
License doesn't exist in this workspace. |
fulkruma_curl POST '/api/v1/licenses/lic_01HX.../revoke' ''
Validate a license (public)
GET /api/v1/licenses/validate?key=<key>&productId=<optional>
Public — no HMAC required. Use this in the buyer's software to check whether a key is still good. Designed to never error on bad input: an unknown or revoked key returns a normal 200 response with valid: false.
Query parameters
| Param | Type | Description |
|---|---|---|
key |
string | The license key. |
productId |
string | Optional. If provided, the key must also belong to this product. |
Response shape
{
"data": {
"valid": true,
"key": "7K3PH-9X2RM-4LBQE-PT86W-JF1RV",
"status": "active",
"productId": "prod_01HX...",
"activations": 1,
"maxActivations": 3,
"expiresAt": null
},
"error": null,
"meta": { ... }
}
For unknown keys, valid is false and the rest of the payload is null. For revoked keys, valid is false with status: "revoked". For expired keys, valid: false, status: "expired".
Activate a license (public)
POST /api/v1/licenses/activate
Binds a license to an instance ID. The first call for a fresh (key, instanceId) pair creates a LicenseActivation row and increments the license's activations counter; subsequent calls for the same pair return alreadyActive: true without side-effects. An instance that was deactivated can activate again (it takes an activation back). The maximum number of distinct concurrent instances is capped at the license's maxActivations — the (maxActivations + 1)th distinct instance returns MAX_ACTIVATIONS.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | The license key. |
instanceId |
string | yes | An identifier for the activating device. Suggestion: hash of (machine ID + product version) so reinstalls reuse the slot. |
Response shape
{
"data": {
"license": {
"id": "lic_01HX...",
"productId": "prod_01HX...",
"customerId": "cus_01HX...",
"key": "7K3PH-9X2RM-4LBQE-PT86W-JF1RV",
"status": "active",
"activations": 1,
"maxActivations": 3,
"expiresAt": null
},
"activation": {
"id": "act_01HX...",
"licenseId": "lic_01HX...",
"instanceId": "machine_a1b2c3",
"activatedAt": "2026-05-12T10:42:00.123Z",
"deactivatedAt": null
},
"alreadyActive": false
},
"error": null,
"meta": { ... }
}
Errors
| Status | error.code |
When |
|---|---|---|
400 |
VALIDATION |
Missing key or instanceId, or key shorter than 8 chars. |
404 |
INVALID_KEY |
Key doesn't exist or has been revoked. |
403 |
EXPIRED |
Key has expired. |
409 |
MAX_ACTIVATIONS |
License is already at its maxActivations cap with this instanceId not among them. |
Deactivate a license (public)
POST /api/v1/licenses/deactivate
Releases an instanceId's slot so it can be activated elsewhere. Idempotent: deactivating an already-deactivated (or never-activated) pair returns 200 with alreadyDeactivated: true.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | The license key. |
instanceId |
string | yes | The instance ID to release. |
Response shape
{
"data": {
"deactivated": true,
"alreadyDeactivated": false,
"activations": 2
},
"error": null,
"meta": { ... }
}
The license object
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
string | no | Fulkruma ID. Always lic_ + 26-char ULID. |
accountId |
string | no | The workspace. |
productId |
string | no | The licensed product. Must be type: license. |
customerId |
string | no | The buyer's ID. |
key |
string | no | The plaintext key (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX). Returned in the list/retrieve response. |
status |
enum | no | active or revoked. |
activations |
integer | no | Currently active instance count. |
maxActivations |
integer | no | Maximum concurrent instances. |
expiresAt |
string | null | yes | Hard expiry, or null for perpetual keys. |
externalSource |
string | yes | Where the issue originated. |
externalRef |
string | yes | Origin system's order ID. |
createdAt |
string (ISO 8601 UTC) | no | Issuance timestamp. |
The activation object
| Field | Type | Description |
|---|---|---|
id |
string (act_…) |
Activation row ID. |
licenseId |
string | The license this binds. |
instanceId |
string | Buyer-supplied instance identifier. |
activatedAt |
string (ISO 8601 UTC) | Initial activation timestamp. |
deactivatedAt |
string | null | When the instance was released, or null if active. |
Events
| Event type | Fires on | Notes |
|---|---|---|
fulkruma.license.issued.v1 |
POST /api/v1/licenses succeeds. Includes auto-issue from the Plugipay-checkout webhook. |
Emitted in the same transaction as the license insert. |
fulkruma.license.revoked.v1 |
POST /api/v1/licenses/:id/revoke succeeds. |
Emitted in the same transaction as the status flip. |
| fulkruma.license.activated.v1 | POST /api/v1/licenses/activate binds a new instance. | Not for an instance already active. |
| fulkruma.license.deactivated.v1 | POST /api/v1/licenses/deactivate unbinds an active instance. | |
Activation events fire only on a real change, not on every check-in: an app that calls activate at each launch sends one event the first time. A subscription to fulkruma.license.* receives them.
See Webhooks for the envelope and signature recipe.
Patterns
Software offline-tolerance
The public validate endpoint takes <200ms and rarely fails, but networks do go down. Cache the last successful validate response for up to 72 hours; on cache miss + failed network, allow the software to run for a configurable grace period. Re-validate on app launch and on a daily cron.
Reinstall on the same device
Pick an instanceId that's stable across reinstalls (hashed machine ID, hardware fingerprint). A fresh install on the same device hits activate with the same instanceId and gets back alreadyActive: true — no slot consumed.
Transferring to a new device
Call /deactivate with the old instanceId, then /activate with the new one. The slot moves cleanly. If the old device is dead and you can't deactivate from it, instruct the buyer to revoke + reissue from your dashboard.
Next
- Products — create
license-type products first. - Deliveries — the equivalent for digital downloads.
- Authentication — HMAC signing for merchant-facing endpoints.