Help center

License Activation v2

Integrate privacy-preserving seats, rotating refresh tokens and offline-verifiable ES256 entitlements.

On this page

1. What activation does

Activation binds one random app installation to a purchased license seat and returns a signed entitlement.

2. Generate an installation ID

Generate UUIDv4 or at least 16 random bytes once per app installation. Keep it in OS-protected local storage.

3. No hardware fingerprint

Do not derive the ID from a MAC address, disk, browser or device serial. Billixi stores only a context-bound HMAC.

4. Call Activate

Send the license key, public product ID, installation ID, SemVer app version and a fresh nonce. Never embed an application secret.

JavaScript · Activate
const installationId = crypto.randomUUID() // persist once in OS-protected storage
const clientNonce = crypto.randomUUID() + crypto.randomUUID()
const response = await fetch('https://api.billixi.com/api/v1/payment/public/licenses/v2/activate', {
  method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ key, product_id: productId, installation_id: installationId,
    app_version: '1.2.3', client_nonce: clientNonce, device_label: 'Work laptop' })
})
if (response.status === 429) scheduleRetry(response.headers.get('retry-after'))
const activation = await response.json()

5. Store the refresh token safely

The opaque refresh token is shown only in the response. Store it in Keychain, Keystore, DPAPI or an equivalent protected facility.

6. Rotate with Refresh

Send the current refresh token once. Replace it atomically with the successor returned by the server; retry a lost response with the same nonce.

JavaScript · Refresh
// Serialize this operation: the current token is single-use.
const next = await post('https://api.billixi.com/api/v1/payment/public/licenses/v2/refresh', {
  refresh_token: secureStore.get('refresh_token'), installation_id: installationId,
  product_id: productId, client_nonce: freshNonce, app_version: '1.2.3'
})
secureStore.replaceAtomically('refresh_token', next.refresh_token)

7. Understand offline grace

The JWT can be verified offline until exp. Revocation cannot invalidate an already issued offline token before that time.

8. Deactivate a seat

Use the current refresh token and installation ID to free the seat, or let the buyer use Library self-service.

POST https://api.billixi.com/api/v1/payment/public/licenses/v2/deactivate
{ refresh_token, installation_id, product_id, client_nonce }

9. Handle seat limits

A seat-limit response means another installation must be deactivated first. Do not invent or reuse hardware fingerprints.

10. Apply update policy

The purchase-time policy snapshot controls versions. Same-major uses strict SemVer 2.0 and downloads still require an Approved exact release.

11. Verify JWT with JWKS

Fetch JWKS, select kid, require ES256 and the custom typ, then validate signature, issuer, audience, nbf and exp.

12. Handle key rotation

Cache JWKS according to response headers and refetch once for an unknown kid. Retiring public keys remain visible until their final tokens expire.

13. Error codes

Treat invalid_key/not_found uniformly in user copy; seat_limit_reached is actionable; reuse and compromised states require reactivation; 429 honors Retry-After.

CodeRecommended action
seat_limit_reachedDeactivate another seat.
version_not_entitledUse a version covered by the purchase policy.
artifact_withdrawnStop distribution and contact the seller.
refresh_token_reuseDiscard the compromised session and activate again.
policy_snapshot_unresolvedContact support for Ops backfill review.
rate_limit_unavailableRetry later; the server is failing closed.

14. Client-side DRM limits

Client-side checks can be removed or patched. Activation raises abuse cost but cannot make a client-controlled application unbreakable.

15. Test environment

Use the public non-production vector to test independent verification. Production credentials, keys and client secrets are never part of the sample.

JWT verification examples

javascript
import { createRemoteJWKSet, decodeProtectedHeader, jwtVerify } from 'jose'

const token = activation.entitlement_token
const header = decodeProtectedHeader(token)
if (header.alg !== 'ES256' || header.typ !== 'billixi-license-entitlement+jwt' || !header.kid)
  throw new Error('Unexpected entitlement header')

const JWKS = createRemoteJWKSet(new URL('https://api.billixi.com/api/v1/payment/public/licenses/v2/.well-known/jwks.json'))
const { payload } = await jwtVerify(token, JWKS, {
  algorithms: ['ES256'],
  issuer: 'https://api.billixi.com',
  audience: 'billixi-license-client',
  typ: 'billixi-license-entitlement+jwt',
  clockTolerance: 60,
})
// jwtVerify checks signature, kid selection, nbf and exp. Now compare product_id/app policy.