Skip to content

Concepts

Webhooks

Get an HTTPS request on your server whenever something important happens in one of your applications.

Events

Webhook events
EventScopeWhen it fires
user.registereduserAn end user registered through the client API.
user.banneduserAn end user was banned.
user.deleteduserAn end user was deleted.
license.redeemedlicenseA license key was redeemed.
license.revokedlicenseA license key was revoked.
entitlement.changedentitlementA user's entitlement for a product changed (granted, frozen, revoked, …).
session.createdsessionAn end user signed in and a new session was created.

Payload

Each delivery is a JSON POST with this envelope:

POST to your endpoint
{
  "id": "evt_…",
  "type": "license.redeemed",
  "createdAt": "2026-11-09T14:03:12.000Z",
  "data": { … }
}
Webhook headers
HeaderFormatDescription
Aroli-Signaturet=<unix>,v1=<hex>v1 is the hex HMAC-SHA256 of "<t>.<raw body>", keyed with your endpoint's secret.
Aroli-Event-Idevt_…Unique per event. Use it to ignore duplicates - a retried delivery keeps the same ID.

Verifying signatures

  1. Read the raw request body before parsing it.
  2. Split Aroli-Signature into t and v1.
  3. Reject the request if t is more than 5 minutes away from your current time.
  4. Compute HMAC-SHA256 of `${t}.${rawBody}` with the endpoint secret and compare it to v1 in constant time.
verify-webhook.ts
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);
}
verify_webhook.py
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 2xx status 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.