Outbound webhooks¶
Berlanggan can push license lifecycle events to your product as they
happen. This is optional and complements — never replaces — the pull-based
POST /v1/validate heartbeat, which stays the
source of truth for licence status.
Enabling¶
In Seller dashboard → Products → (product) → Developer webhook, set:
| Field | Purpose |
|---|---|
| Webhook URL | HTTPS endpoint that receives POST requests. |
| Signing secret | Shared secret used to sign each request (HMAC-SHA256). Keep it out of source control. |
Leave the URL blank to disable. Events are only sent for products that carry a
license_key deliverable.
Events¶
| Event | Fired when |
|---|---|
license.issued |
A paid order provisioned a licence (order.paid). |
license.renewed |
A subscription renewed for another period (subscription.renewed). |
license.suspended |
A subscription was suspended and access revoked (subscription.suspended). |
All three carry the same payload shape.
Request¶
POST https://your-app.example.com/berlanggan/webhook
Content-Type: application/json
User-Agent: Berlanggan-Webhook/1.0
X-Berlanggan-Signature: sha256=1f8ac10f23c5b8...
{
"event": "license.issued",
"order_public_id": "ord_9F3K2M7QP1",
"customer_email": "buyer@example.com",
"license_key": "XXXX-XXXX-XXXX",
"plan": "InTourney Pro",
"license_expires_at": "2027-06-01T00:00:00Z",
"entitlements": { "SEATS": "5", "API_ACCESS": "true" },
"seat_limit": 1
}
| Field | Notes |
|---|---|
event |
One of the three event names above. |
order_public_id |
The order that created or last renewed the licence. May be "" for a legacy subscription with no linked order. |
customer_email |
The buyer's account e-mail. |
license_key |
XXXX-XXXX-XXXX (Crockford Base32). |
plan |
The plan name at time of sending. |
license_expires_at |
ISO-8601 UTC, or null for a perpetual (one-time) licence. On license.renewed this is the new period end. |
entitlements |
Flat {key: value} map — the same values returned by the activation API. Use these for feature gating. |
seat_limit |
Integer seat count for the licence. |
Verifying the signature¶
X-Berlanggan-Signature is sha256= followed by the hex HMAC-SHA256 of the
raw request body, keyed by your signing secret. This is the same scheme
Berlanggan uses to verify inbound webhooks from Duitku, Sumopod and kirim.chat.
Verify against the exact bytes you received — do not re-serialize the JSON first.
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const provided = (header || "").replace(/^sha256=/, "");
const expected = crypto.createHmac("sha256", secret)
.update(rawBody).digest("hex");
return provided.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
}
Responding¶
- Return 2xx as soon as you have stored the event. Do slow work asynchronously.
- Any non-2xx response, a timeout (10 s), or a connection error triggers a retry with exponential backoff (up to ~6 attempts over roughly an hour).
- Deliveries are at-least-once and may arrive out of order. De-duplicate
on
(event, order_public_id, license_key)and always trust the newestlicense_expires_at/entitlements.
Security checklist¶
- [ ] Reject any request whose signature does not verify.
- [ ] Reject requests without HTTPS.
- [ ] Rotate the signing secret if it may have leaked (update it in the dashboard, then deploy the new value).
- [ ] Treat the webhook as a hint — confirm critical state changes with
POST /v1/validatebefore permanently locking a customer out.