Skip to main content

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

POST/webhooks

Register a new webhook endpoint. Requires API Key auth with webhooks:write scope.

Request Body

ParameterTypeRequiredDescription
urlstringRequiredHTTPS webhook endpoint URL (no private IPs or localhost)
eventsstring[]OptionalEvent types to subscribe to (min 1, default: ["*"] for all events)
Request
{
  "url": "https://example.com/webhooks/licentric",
  "events": ["license.created", "license.suspended", "machine.activated"]
}
201Webhook registered
json
{
  "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

GET/webhooks

List registered webhook endpoints. Requires API Key auth with webhooks:read scope.

Query Parameters

ParameterTypeRequiredDescription
cursoruuidOptionalPagination cursor from previous response
limitintegerOptionalResults per page (1-100, default 25)
Request
GET /api/v1/webhooks?limit=25
200Paginated list
json
{
  "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.

EventDescription
license.createdA new license was created
license.updatedA license was updated
license.suspendedA license was suspended
license.reinstatedA suspended license was reinstated
license.revokedA license was permanently revoked
license.renewedA license expiration was extended
license.expiredA license reached its expiration date
license.deletedA license was soft-deleted
machine.activatedA machine was activated
machine.deactivatedA machine was deactivated
machine.heartbeatA machine heartbeat was received
machine.deadA machine missed its heartbeat window
validation.successA license validation succeeded
validation.failedA 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).

Webhook Payload
{
  "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.
Headers
X-Licentric-Signature: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
X-Licentric-Timestamp: 1745856000
X-Licentric-Event: license.created
X-Licentric-Delivery: 8c3a4d5e-1234-5678-9abc-def012345678

Node.js Verification

verify-webhook.ts
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

verify_webhook.py
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 SDK
# 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 SDK
// 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;
}
Always Verify Signatures + Timestamp
Verify the HMAC-SHA256 signature AND reject payloads with timestamps older than 5 minutes. Use constant-time comparison to prevent timing attacks. Skipping the timestamp check exposes you to replay attacks.
Delivery Guarantees
Webhooks are delivered at least once. Your endpoint should be idempotent and use the event id to deduplicate. Failed deliveries are retried with exponential backoff.