Webhooks
KeyGrant sends events about your licenses and devices, such as a key issued or a device activated, to your own HTTPS endpoints. The full list is on Webhook events.
Add an endpoint
In the dashboard, under Developers > Webhooks, choose Add endpoint:
- Endpoint URL:
https://on a public host (notlocalhost,.local,.internalor a private address), up to 500 characters. It must respond within 10 seconds. - Name, for your own reference, optional.
- Events to send: at least one.
license.issuedandlicense.revokedare selected by default. - Send test license events: off by default. Events about test licenses go only to
endpoints with this setting on, and are marked
test: true. Live events go to every endpoint that subscribes to them.
Manage an endpoint
- Signing secret: shown when you choose Reveal:
whsec_followed by 48 hex characters. - Roll: replaces the secret immediately. Deliveries are never signed with the old secret again.
- Disable: stops deliveries to the endpoint. A retry that comes due while it is disabled is marked Parked without being sent.
- Delete endpoint: removes the endpoint and its delivery history.
Warning
Delete endpoint is for good: deliveries still waiting to be sent or tried again are dropped, and adding the URL again makes a new endpoint with a new signing secret.
The request
Each event is a POST with a JSON body:
{ "id": "evt_9b2c7e1f0a4d4c3b8e6f5a2d1c0b9e8f", "type": "license.issued", "created": 1791257861000, "version": "2026-06-01", "data": { "key": "MYPROD-9K4T-2M8P-XQ7V-H3DW", "product": "my-product", "email": "[email protected]", "model": "perpetual", "modelId": "mod_5c1e0a7b9d2f4e8a6b3c1d0e9f8a7b6c", "source": "checkout" }} id: the event's ID, the same for every endpoint and every retry of it.type: the event type (see Webhook events).created: when it happened, in Unix milliseconds.version: the payload version,2026-06-01.data: the event's fields.test:trueon an event about a test license. Absent otherwise.
With these headers:
KeyGrant-Signature: t=1791257861,v1=5f0c…e41aKeyGrant-Event-Id: evt_9b2c7e1f0a4d4c3b8e6f5a2d1c0b9e8fKeyGrant-Delivery-Attempt: 1user-agent: KeyGrant-Webhooks/1content-type: application/json Verify the signature
Check KeyGrant-Signature before you trust the payload:
- Read the request body exactly as received, before any JSON parsing.
-
Take
t(the time this attempt was signed, in Unix seconds) andv1from the header. -
Compute the HMAC-SHA256 of
t, a dot and the raw body, keyed with the whole signing secret (whsec_included), in lowercase hex. - Compare it with
v1in constant time. - Refuse a
tmore than 5 minutes from now.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received, before any JSON parsing.function verifyKeyGrantSignature(rawBody, header, secret, toleranceSeconds = 300) { if (typeof header !== "string") return false; const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2))); const t = Number(parts.t); if (!Number.isInteger(t) || typeof parts.v1 !== "string") return false; if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const given = Buffer.from(parts.v1, "utf8"); const wanted = Buffer.from(expected, "utf8"); return given.length === wanted.length && timingSafeEqual(given, wanted);} - Each attempt is signed when it is sent, so a 5-minute tolerance on
tworks for retries too. - Respond with a 2xx status within 10 seconds and do the work afterward.
-
A delivery can arrive more than once (for example, when your handler timed out after it had already
acted), so use
idto process each event only once.
Deliveries and retries
A delivery succeeds on a 2xx status. A redirect is not followed and counts as a failure, as do a timeout and any other status.
- When one sale's keys are issued, revoked or reinstated together, the first six deliveries go at once and the rest within a few minutes, up to 120 a minute. Until it is sent, a delivery reads queued.
- Each attempt carries its number in
KeyGrant-Delivery-Attempt.
When a failed delivery is tried again
A failed delivery is retried, waiting longer after each failure, for up to 9 attempts in all:
| Attempt | Sent |
|---|---|
| 1 | When the event happens |
| 2 | 1 minute after the first failure |
| 3, 4, 5 | 5, 15 and 45 minutes after the failure before |
| 6, 7, 8, 9 | 2, 4, 8 and 12 hours after the failure before |
A delivery that fails all 9 attempts is marked Parked.
An endpoint that keeps failing
- KeyGrant never disables an endpoint for failing.
- When an endpoint has 10 or more failed deliveries in the last 24 hours, KeyGrant sends the Webhook endpoint failing alert, at most once a day.
- It goes to your account's developers by default (Settings > Alerts).
Event deliveries
The endpoint's Event deliveries tab lists every delivery for 30 days, with the request sent and your handler's response.
- Retry this delivery: sends a failed delivery again at once, unless a retry is already sending it. Then the delivery says so and shows the result once it lands.
- Retry all failed: queues up to 500 of the newest failed deliveries, each with a new budget of 9 attempts. They go out up to 120 a minute.
- Neither retry button works while the endpoint is disabled.
Send a test event
Send a test event sends one request whose type is the first event in the
endpoint's list, marked test: true, regardless of the endpoint's settings. Its
data always holds test: true, key: "SAMPLE-0000-0000-0000-0000"
and product: "sample".