Documentation menu▾

Webhooks

Get notified when a license is created, suspended, revoked or activated. Payload format, HMAC signature verification and how retries work.

Webhooks tell your other systems when something happens to a license. A CRM can tag a customer when they buy, a SaaS backend can lock an account when a license is revoked, and a chat channel can announce every new sale. Keygate sends an HTTPS POST to your endpoint for each event you subscribe to.

Add an endpoint

Open Webhooks in the dashboard, enter the URL that should receive events and choose the events you want. Keygate generates a signing secret for the endpoint; store it on your side, you need it to check that a request really came from Keygate.

Use the test button to send a webhook.test event and see it arrive. Every delivery is listed with its response code, and any of them can be sent again from there.

What a delivery looks like

The body is JSON with the event name, a timestamp and the data for that event:

json
{
  "event": "license.created",
  "timestamp": "2026-10-04T09:12:44Z",
  "data": {
    "license_id": "4f0c6c1e-2a7b-4d4e-9a55-0d3b7e1f2c11",
    "email": "[email protected]"
  }
}

Three headers come with it:

HeaderContains
X-Keygate-EventThe event name, the same as in the body.
X-Keygate-Signaturesha256= followed by the HMAC of the body, in hex.
X-Keygate-DeliveryA unique ID for this delivery.

Verify the signature

Compute an HMAC SHA256 of the raw request body with your endpoint's secret and compare it with the header. Use the body exactly as it arrived; parsing and serializing the JSON again changes the bytes and the signature no longer matches.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function isFromKeygate(rawBody, signatureHeader, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}
Go
func isFromKeygate(body []byte, signatureHeader, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signatureHeader))
}

Answer with any 2xx status once you have accepted the event. Do slow work after you reply, because Keygate waits ten seconds by default before it treats the delivery as failed.

Events

EventSent when
license.createdA license is created by a checkout, in the dashboard or through the API.
license.activatedA device or user is activated on a license.
license.deactivatedA device is removed from a license: by your app, in the customer portal or in the dashboard.
license.suspendedA license is suspended by an admin, or its subscription is paused in Stripe.
license.reinstatedA suspended license is back in use.
license.revokedA license is revoked in the dashboard, or fully refunded in the dashboard or in Stripe.
license.canceledThe subscription behind a license ends.
license.expiredA license passes its end date and grace days.
license.expiry_changedAn admin changes a license's end date.
license.payment_failedA subscription payment fails.
license.payment_recoveredA failed payment goes through after all.
plan.changedA license moves to another plan, in the dashboard or the customer portal.
seat.addedA team member joins a license.
seat.removedA team member leaves a license.
quota.warningA license crosses the warning level of a usage quota.
quota.exceededA license uses up a usage quota.
release.publishedA release is published.
release.yankedA release is pulled.
release.unyankedA pulled release is put back.

Retries and duplicates

If your endpoint does not answer with 2xx, Keygate tries again after one minute, then after two, four and eight minutes. It gives up after five attempts in all, a number you can change with WEBHOOK_MAX_ATTEMPTS.

  • Automatic retries keep the same X-Keygate-Delivery ID, so you can skip an ID you have already processed.
  • A delivery sent again by hand from the dashboard gets a new ID but carries exactly the same body. If an event must never be processed twice, remember the event name, the timestamp and the license ID together.

Private addresses

To protect your network, Keygate refuses to deliver to private, loopback and link local addresses by default. If your receiver runs on the same machine or Docker network, set WEBHOOK_ALLOW_PRIVATE=true.

Last updated October 4, 2026