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#
"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:
POST /v1/activate
{ "serial": "...", "domain": "example.com", "host": "...", "app_version": "1.2.0" }
A success carries the signed token and the entitlement:
{
"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:
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:
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:
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.