Skip to content
Bondry

License API for module authors

How a paid module licenses itself: the manifest key, the activation call, the heartbeat and the degradation policy.

A paid module is licensed by the same server the core talks to, using the same signed transport. You declare it in the manifest; the kernel does the rest.

The manifest key#

JSON
"licensing": { "product_slug": "your-module" }

Present means the module is licensed: the panel shows a License screen for it, and the module can ask whether updates are allowed. Absent means the module is free and nothing is checked. The factory modules that ship with the core (payments, gamification) carry no licensing key at all.

The product_slug is what the license server knows the product by, and it is what lands in the prd claim of the token.

Activation#

The member enters the serial in Modules > License. The kernel calls the server:

Code
POST /v1/activate
{ "serial": "...", "domain": "example.com", "host": "...", "app_version": "1.2.0" }

A success carries the signed token and the entitlement:

JSON
{
  "ok": true,
  "token": "<JWT RS256>",
  "license": { "serial": "...", "product": "your-module", "updates_until": 1789000000, "domain": "example.com" },
  "features": []
}

A refusal comes back with ok: false and a reason: invalid_serial, domain_in_use, revoked or rate_limited. It arrives as HTTP 200; a 4xx means the server is unavailable, which is a different situation entirely.

A module license activates on the same production domain as the core license.

The signed transport#

Every call to /v1/* carries four headers:

Code
X-Bondry-Key:        the identifier of the shared key
X-Bondry-Timestamp:  unix time, valid for 300 seconds
X-Bondry-Nonce:      uuid v4, single use
X-Bondry-Signature:  hex(hmac_sha256(secret, METHOD\nPATH\nTIMESTAMP\nNONCE\nsha256(body)))

You never build these yourself: the kernel's license client signs the call. It is documented here so you can recognise it in a log.

The token#

RS256, verified locally against the public key shipped with Bondry. Claims: iss, sub (the serial), prd (your product slug), dom (the activated domain), upd (the end of the update window), feat, iat, exp (30 days) and jti.

The token is stored encrypted and never leaves the install.

The heartbeat#

Daily, from the scheduler:

Code
POST /v1/heartbeat
{ "token": "...", "domain": "example.com", "app_version": "1.2.0" }

The answer refreshes the token when it is close to expiring and returns the current updates_until plus flags for revoked and expired, with an optional human message shown in the panel.

If the server cannot be reached, nothing degrades for fourteen days. Your module must not treat a network error as a refusal.

The degradation policy#

This is a product rule, not a suggestion.

State What your module may do
Active Everything
Update window expired Everything the buyer already has. Updates unavailable.
Revoked Everything the buyer already has. A persistent notice in the panel. Updates unavailable.
Offline past the grace period Same as expired, reversible on reconnection

Never block content or basic functionality. A license controls updates and marked extras, not the right to keep using what was bought. A module that locks a buyer out of their own data will not be accepted, and it is the fastest way to earn a chargeback.

Asking about the license#

Ask the kernel, do not call the server yourself:

PHP
if (\App\Kernel\License\ModuleLicenses::allowsUpdates('your-module')) {
    // Offer the update.
}

Testing without a server#

During development the license client can run with no server configured. It records the intent and stays unlocked, so you are never blocked on network access while writing a module.