$ feature / webhooks

Real-time HTTP for license events.

Every license lifecycle action emits a webhook. Configure an endpoint per app (or several), receive signed JSON payloads in real time, and pipe the events wherever you want: a Slack channel for support visibility, a Postgres table for analytics, your own backend for fulfilling licenses against orders, or a Discord bot for "new sale" pings.

Event catalog

license.created

A new license has been issued (manually from the dashboard, programmatically through the developer API, or automatically by a Stripe / Lemon Squeezy commerce flow). Payload includes the license key, the issuing app id, and any metadata you attached at creation time.

license.validated

A successful validation has been processed. Use this for analytics ('how many active users do I have today'), session monitoring, or to push a row into your own data warehouse. Failed validations are not delivered as events.

license.revoked

A license has been revoked, either manually, through a refund event, or by the self-ban flow. Use this to lock the customer out of any additional services your backend exposes.

license.activated

A license has just been bound to its first HWID. Useful for first-launch analytics ('this user actually opened the app') or for sending a welcome email with onboarding tips.

license.hwid_bound

A new HWID has been added to a license's seat list (filling slot N of M). The payload includes the slot index, the new HWID hash, and the source IP that bound it.

license.hwid_reset

All HWIDs have been cleared from a license. Useful for audit trails (who reset what, when) and for triggering a tier-2 review if the same license is reset frequently.

license.deleted

A license has been permanently deleted. This is irreversible: the key cannot be reactivated or rebound after this event fires.

Signed payloads (HMAC-SHA256)

Each delivery includes an X-AuthForge-Signature header that is the HMAC-SHA256 of the raw request body, keyed by your webhook secret. Compute the same HMAC on your side, do a constant-time compare, and reject anything that doesn't match. This stops anyone who finds your webhook URL from forging an event.

// Node.js example using the standard crypto module
import crypto from "node:crypto";

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signatureHeader, "hex"),
  );
}

The webhook secret is generated when you create the endpoint in the dashboard (App settings → Webhooks → New endpoint) and is shown exactly once. Rotate it whenever a developer leaves your team.

Test delivery

Each webhook endpoint has a Send test event button in the dashboard. Pick the event type you want to simulate and the dashboard delivers a synthetic event with the same payload shape and headers, signed with your real webhook secret, to your endpoint. Use it while developing your handler; you don't have to drum up a real refund or manual revocation just to verify your code works.

Retries and replay

Failed deliveries (any non-2xx HTTP response or a network timeout) are retried with exponential backoff over 24 hours. Every event, successful or not, is logged on the endpoint detail page in the dashboard. Failed events can be replayed manually from the same page, which is the typical recovery path after a deploy that broke your handler.

Each event carries a unique id. Make your handler idempotent keyed on that id; AuthForge guarantees at-least-once delivery, not exactly-once.

Related