Webhooks
Webhooks notify your server in real-time when events occur in your Licentric account. Register HTTPS endpoints to receive POST requests with event payloads.
Register Webhook
/webhooksRegister a new webhook endpoint. Requires API Key auth with webhooks:write scope.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | Required | HTTPS webhook endpoint URL (no private IPs or localhost) |
| events | string[] | Optional | Event types to subscribe to (min 1, default: ["*"] for all events) |
{
"url": "https://example.com/webhooks/licentric",
"events": ["license.created", "license.suspended", "machine.activated"]
}{
"data": {
"id": "b3c4d5e6-8f17-9203-e467-df1a2b3c4d5e",
"url": "https://example.com/webhooks/licentric",
"events": ["license.created", "license.suspended", "machine.activated"],
"accountId": "a0b1c2d3-7f15-8192-d356-ce0f1a2b3c4d",
"createdAt": "2026-01-20T16:00:00.000Z",
"updatedAt": "2026-01-20T16:00:00.000Z",
"deletedAt": null
}
}List Webhooks
/webhooksList registered webhook endpoints. Requires API Key auth with webhooks:read scope.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| cursor | uuid | Optional | Pagination cursor from previous response |
| limit | integer | Optional | Results per page (1-100, default 25) |
GET /api/v1/webhooks?limit=25{
"data": [
{
"id": "b3c4d5e6-8f17-9203-e467-df1a2b3c4d5e",
"url": "https://example.com/webhooks/licentric",
"events": ["license.created", "license.suspended", "machine.activated"],
"createdAt": "2026-01-20T16:00:00.000Z",
"updatedAt": "2026-01-20T16:00:00.000Z",
"deletedAt": null
}
],
"pagination": { "nextCursor": null, "hasMore": false }
}Event Types
Subscribe to specific events or use * to receive all events.
| Event | Description |
|---|---|
| license.created | A new license was created |
| license.updated | A license was updated |
| license.suspended | A license was suspended |
| license.reinstated | A suspended license was reinstated |
| license.revoked | A license was permanently revoked |
| license.renewed | A license expiration was extended |
| license.expired | A license reached its expiration date |
| license.deleted | A license was soft-deleted |
| machine.activated | A machine was activated |
| machine.deactivated | A machine was deactivated |
| machine.heartbeat | A machine heartbeat was received |
| machine.dead | A machine missed its heartbeat window |
| validation.success | A license validation succeeded |
| validation.failed | A license validation failed |
Webhook Payload
Webhook payloads are sent as POST requests with a JSON body using a standard envelope shape. The envelope is stable across all event types — only the data field varies. Use the top-level id for idempotent processing (deduplicate replays at your endpoint).
{
"id": "evt_c4d5e6f79a280314f578e01a2b3c4d5e",
"type": "license.created",
"createdAt": "2026-03-01T14:00:00.000Z",
"data": {
"id": "c8f4e9a2-3b71-4d5e-9f12-8a6b3c7d4e5f",
"key": "DSK-A1B2-C3D4-E5F6-G7H8",
"status": "active",
"productId": "d9e5f0a1-4c82-5e6f-a023-9b7c4d8e5f60"
}
}Signature & Replay Protection
Every webhook request includes two security headers. Verify both before trusting the payload.
X-Licentric-Signature— lowercase-hex HMAC-SHA256 of"<timestamp>.<body>"using your webhook secret.X-Licentric-Timestamp— Unix timestamp (seconds) when the request was signed. Reject payloads older than 5 minutes to prevent replay attacks.
X-Licentric-Signature: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
X-Licentric-Timestamp: 1745856000
X-Licentric-Event: license.created
X-Licentric-Delivery: 8c3a4d5e-1234-5678-9abc-def012345678Node.js Verification
import { createHmac, timingSafeEqual } from "crypto";
const REPLAY_TOLERANCE_SECONDS = 300; // 5 min
function verifyWebhookSignature(
body: string,
signature: string,
timestamp: string,
secret: string
): boolean {
// 1. Replay protection: reject stale timestamps
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (age > REPLAY_TOLERANCE_SECONDS) return false;
// 2. HMAC over "<timestamp>.<body>"
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
return timingSafeEqual(
Buffer.from(signature, "utf-8"),
Buffer.from(expected, "utf-8")
);
}Python Verification
import hmac
import hashlib
import time
REPLAY_TOLERANCE_SECONDS = 300 # 5 min
def verify_webhook_signature(
body: bytes, signature: str, timestamp: str, secret: str
) -> bool:
# 1. Replay protection: reject stale timestamps
age = int(time.time()) - int(timestamp)
if age > REPLAY_TOLERANCE_SECONDS:
return False
# 2. HMAC over "<timestamp>.<body>"
material = timestamp.encode() + b"." + body
expected = hmac.new(secret.encode(), material, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)Using the SDK
Both the Python and TypeScript SDKs ship a verifyWebhook / verify_webhook helper that performs signature, timestamp, and envelope checks in one call.
# Python
from licentric import verify_webhook, WebhookSignatureError
try:
event = verify_webhook(
body=request.body,
signature=request.headers["X-Licentric-Signature"],
timestamp=request.headers["X-Licentric-Timestamp"],
secret=WEBHOOK_SECRET,
)
except WebhookSignatureError as exc:
return ("rejected: " + exc.reason, 401)
process_event(event)// TypeScript
import { verifyWebhook, WebhookSignatureError } from "@licentric/sdk";
try {
const event = verifyWebhook({
body: req.rawBody,
signature: req.headers["x-licentric-signature"] as string,
timestamp: req.headers["x-licentric-timestamp"] as string,
secret: process.env.WEBHOOK_SECRET!,
});
await processEvent(event);
} catch (err) {
if (err instanceof WebhookSignatureError) {
return res.status(401).send(`rejected: ${err.reason}`);
}
throw err;
}id to deduplicate. Failed deliveries are retried with exponential backoff.