Activation
Quick summary
Activation binds a license key to a device public key and returns a short-lived entitlement if the server approves. Verification-only lookup does not register a device.
Use the request/response example below, then check the response status and device limit before storing the result.
Activation converts a license key + a device public key into a bound device slot and a short-lived entitlement.
Request/response (identical body on all paths)
POST {activatePath}
{ "key": "NG-XXXX-XXXX", "product_id": "prod_…", "public_jwk": { … } }2
{ "entitlement": "<jws>", "expires_in": 120, "features": ["…"] }The only difference between backends is the route.
Activation paths
| Surface | Route | Model |
|---|---|---|
be-cloudflare (PoP) | POST /api/v1/licenses/activate | validates public_jwk on-curve → license gate check → atomic license_bindings slot (UNIQUE(lic_id,jkt), count < max_devices) → entitlement |
@yetdapay/license-server (BYOS) | POST /api/license/activate (DEFAULT_ACTIVATE_PATH) | same body/response; PoC/Nuxt-style self-hosted |
| SDK worker-preset | POST /api/v1/licenses/activate (WORKER_ACTIVATE_PATH, createWorkerConfig()) | same wire format, Worker route |
Axum be/ (legacy only) | POST /api/v1/licenses/verify + POST /licenses/:id/activations | activations_json display records — no PoP, no entitlement issued |
What the server checks
public_jwk— normalized and verified on-curve (invalid public_jwk→400).- License gate — key exists,
product_idmatches the live license row (drift → 401/403 PRODUCT_MISMATCH), statusactive, not expired. - Device slot —
countBindings(lic_id) < max_devices; binding(lic_id, jkt)is unique — re-activating the same device is idempotent, a new device beyond the limit →DEVICE_LIMIT.
Error codes (activation)
| Failure | Worker | SDK |
|---|---|---|
| malformed body / bad JWK | 400 VALIDATION | 400 VALIDATION / 400 INVALID_PUBLIC_JWK |
| unknown key | 403 NOT_FOUND ("license not activatable") | 404 NOT_FOUND |
| product mismatch | 403 PRODUCT_MISMATCH | 403 PRODUCT_MISMATCH |
| revoked/suspended/expired | 403 same code | 403 same code |
unknown status | 403 INVALID_STATUS | 403 REVOKED (fail closed) |
| device limit | 403 DEVICE_LIMIT | 403 DEVICE_LIMIT |
The 403/404 NOT_FOUND delta and the INVALID_STATUS→REVOKED fail-closed delta are documented, deliberate — see the contract matrix §2.
Legacy path (no PoP)
POST /api/v1/licenses/verify (public, both backends) is a lookup-only verdict — {valid, code, status, product_id, product_name, expires_at, max_devices, activated_devices, device_registered, checked_at} with codes OK|NOT_FOUND|PRODUCT_MISMATCH|REVOKED|SUSPENDED|EXPIRED|INVALID_STATUS — and does not consume slots. POST /licenses/:id/activations / DELETE /licenses/:id/activations/:device (seller-owner) manage the legacy device_id records for display only.