Publisher program
Register your module, publish a version, get it signed, and receive the events your server needs.
We do not host your module. You sell and distribute it wherever you want: your own site, your own store, a marketplace, a reseller. What we keep is the truth about the file, and that truth is what the buyer's installer checks before it extracts a single byte.
What you do, once#
- Register the artifact in your account, under Publishing. The slug is
reserved here, not on your first release: it is the name your module carries
into every installation, it goes into your
module.json, and it never changes. Names in our namespace (bondry-,core,admin,designer,members,lms) are refused. - Generate the publishing token. It is a
bkp_id plus a secret shown once. One token per artifact, revocable on the spot. Revoking does not take down what is already published: it stops the next announcement. - Set your webhook URL if you want your server to hear from us. Https only, port 443, and the name has to resolve to public addresses.
What your server does, every release#
Everything is signed with the same envelope you already know from the license API, so there is nothing new to learn:
X-Bondry-Key: bkp_3f7a91c25e08
X-Bondry-Timestamp: 1789459200
X-Bondry-Nonce: 3f7a91c25e0844b1
X-Bondry-Signature: hex(hmac_sha256(secret, METHOD\nPATH\nTIMESTAMP\nNONCE\nsha256(body)))
The timestamp window is 300 seconds, the nonce is single use, and the body enters the signature through its sha256, so not a byte changes on the way.
| Call | What it does |
|---|---|
POST /v1/publisher/artifacts/{slug} |
Updates name, summary, description per language, website and support URL. Idempotent: a field you leave out is a field we leave alone. English is required in every localised text |
POST /v1/publisher/artifacts/{slug}/releases |
Announces a version: version, min_core, max_core, php, notes per language, size, sha256 |
PUT /v1/publisher/releases/{version}/package |
Sends the zip, raw in the body, up to 40 MB. The sha256 has to match what you announced |
GET /v1/publisher/artifacts/{slug} |
Everything your pipeline needs to decide whether to go on: review state, published version, validation hash, last announcement |
GET /v1/publisher/releases/{version}/package |
After approval, a short signed URL to collect the signed zip |
GET /v1/publisher/releases/{version}/hash |
The validation hash of that version |
Announcing is cheap and repeatable; uploading 40 MB is not, which is why they are two calls. An announcement without a package expires on its own after seven days, and that version number goes back to being free.
Every refusal tells you the field, what arrived and what was expected. "Invalid payload" is not a message.
The review#
The automatic half runs the moment your package lands: no path outside the
archive, no symlink, no zip bomb, size within the cap, a valid manifest with the
slug you registered and the version you announced, a version that moves forward,
a min_core that exists, an English translation present, and a static scan that
raises a flag on eval, a large base64_decode literal, shell_exec, a URL
with a fixed IP address, and obfuscated code.
A flag does not reject anything on its own: legitimate code uses all of them now and then. It goes to a human reviewer with the exact file and line.
The human half looks at your manifest, the permissions and hooks you declare, the migrations and route files in the package, the flags, and the difference from your previous version. The decision is approve, reject, ask for changes or suspend, and every one of them but approval comes with a written reason that reaches you word for word.
"Changes requested" puts the version back to draft, which is the only state where your server can send the package of the same version again.
The signed zip is yours to distribute#
Approved, we sign your zip with the Bondry package key and record the sha256 of the signed file. That hash is the identity of that version forever.
The signed zip is your zip plus two entries at the root:
bondry-artifact.json {"slug":…,"version":…,"publisher":…,"files":{"<path>":"<sha256>"}}
bondry-artifact.sig base64 RSA-SHA256 over the canonical manifest
The file list covers every file entry of your original zip, and the installer
checks it both ways: nothing in the zip missing from the list, nothing in the
list missing from the zip. Without that, someone could add a .php to the
package after signing and the signature would still verify.
You collect the signed file from your account or from the API, and we delete our copy as soon as you download it, and in any case seven days after approval, with an e-mail on the third day if you have not collected it. What stays here is the manifest, the signature, the hash and the notes. The file is yours, and so is keeping it.
What the buyer's installer does#
It computes the sha256 of the file in front of it, checks the embedded signature with the public key it already carries for core updates, and asks the registry:
GET /v1/artifacts/{slug}/verify?version=1.4.2&sha256=<64 hex>
No credential, one of three answers, always HTTP 200:
| Answer | What the buyer sees |
|---|---|
verified |
"Checked against the Bondry registry, version 1.4.2 by " and the install goes on |
altered |
A red wall: this file is not what the author published. The install refuses by default |
unknown |
Not in the registry: treated like any zip downloaded from the internet |
A suspended version answers unknown. With no network the embedded signature
still holds and the installer says it could not confirm: a missing network is
never an accusation, and never a silent approval.
The events you receive#
You register one https URL per artifact and we call it:
| Event | When |
|---|---|
artifact.approved / artifact.rejected |
the decision on your registration |
release.approved |
the version passed review and is published |
release.rejected |
rejected, or sent back for changes, with the reason |
release.suspended |
taken down after publication, with the reason |
package.altered |
a licensed installation received a file that does not match your approved hash |
The body:
{
"id": "01J8ZC5E7Q2R8VQ1F0M4V8N0PA",
"type": "release.approved",
"created_at": "2026-09-17T11:31:55+00:00",
"data": { "slug": "directory", "version": "1.1.0", "validation_hash": "…" }
}
The id is stable: a resend carries the same id, so treat it as your
idempotency key. Where a type covers more than one decision, data.decision
carries the exact word and data.reason the written reason.
Checking the signature#
We sign the call with the webhook secret of that artifact, in the same envelope
as above, with one difference: X-Bondry-Key carries whk_<12 hex>, the public
id of the secret. That is how your side knows which secret to use after a
rotation, and it is why you should key your stored secrets by that id.
$canonical = implode("\n", [
'POST',
'/your/webhook/path',
$request->header('X-Bondry-Timestamp'),
$request->header('X-Bondry-Nonce'),
hash('sha256', $request->getContent()),
]);
$expected = hash_hmac('sha256', $canonical, $yourSecretFor($request->header('X-Bondry-Key')));
if (! hash_equals($expected, (string) $request->header('X-Bondry-Signature'))) {
abort(401);
}
Rotating the secret from your account keeps the previous one valid for 24 hours, so you can change your configuration without losing an event.
Answer 2xx within 10 seconds. Otherwise we retry after 1 min, 5 min, 30 min,
2 h, 12 h and 24 h, and then the endpoint goes into quarantine and you get an
e-mail. Every delivery and every attempt is visible in your account, with the
response your server gave and a button to send it again.
When someone ships an altered copy#
The verdict call is public, so it never raises an alert: anyone could call it a thousand times with an invented hash, and the registry would become a spam amplifier pointed at you.
What does raise an alert is the event: an installation with an active licence
reporting that the file it received is not the one we signed. That becomes the
package.altered webhook, an e-mail to you with the hash received and the hash
we approved, and a line in our queue, grouped by artifact and hash. The same
altered hash showing up across many licences is piracy at scale, and that is
exactly what the grouping is there to show.
Nothing is revoked automatically by any of this. Suspending is a person, with a written reason.