Concepts
Webhooks
Get an HTTPS request on your server whenever something important happens in one of your applications.
Events
| Event | Scope | When it fires |
|---|---|---|
| user.registered | user | An end user registered through the client API. |
| user.banned | user | An end user was banned. |
| user.deleted | user | An end user was deleted. |
| license.redeemed | license | A license key was redeemed. |
| license.revoked | license | A license key was revoked. |
| entitlement.changed | entitlement | A user's entitlement for a product changed (granted, frozen, revoked, …). |
| session.created | session | An end user signed in and a new session was created. |
Payload
Each delivery is a JSON POST with this envelope:
{
"id": "evt_…",
"type": "license.redeemed",
"createdAt": "2026-11-09T14:03:12.000Z",
"data": { … }
}| Header | Format | Description |
|---|---|---|
| Aroli-Signature | t=<unix>,v1=<hex> | v1 is the hex HMAC-SHA256 of "<t>.<raw body>", keyed with your endpoint's secret. |
| Aroli-Event-Id | evt_… | Unique per event. Use it to ignore duplicates - a retried delivery keeps the same ID. |
Verifying signatures
- Read the raw request body before parsing it.
- Split
Aroli-Signatureintotandv1. - Reject the request if
tis more than 5 minutes away from your current time. - Compute HMAC-SHA256 of
`${t}.${rawBody}`with the endpoint secret and compare it tov1in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the exact request body as a string - before any JSON parsing.
export function verifyAroliWebhook(rawBody: string, header: string | null, secret: string) {
const parts = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
const v1 = String(parts.v1 ?? "");
if (!t || !v1) return false;
// Reject events older (or newer) than 5 minutes to stop replays.
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}import hmac, hashlib, time
def verify_aroli_webhook(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1 or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Delivery and retries
- Respond with any
2xxstatus to acknowledge a delivery. Do slow work asynchronously. - Failed deliveries are retried with exponential backoff, up to 8 attempts.
- Deliveries can arrive more than once or out of order - deduplicate on Aroli-Event-Id.
Endpoint requirements
Public HTTPS only
Webhook URLs must use HTTPS and resolve to a public address. Private and loopback addresses are rejected to prevent server-side request forgery. For local development, use a tunnelling service that gives you a public HTTPS URL.
Each endpoint has its own signing secret. It is stored encrypted at rest on our side; keep it out of source control and treat it like a password.