Webhooks

v1 JSON over HTTPS Bearer credentials or HMAC signatures

Receive licence and release events instead of polling.

Available events

license.created · license.activated · license.deactivated · license.expired · license.suspended · license.revoked · license.blocked · license.restored · installation.deactivated · software.released · software.downloaded · security.alert

Payload

{
  "id": "0f7c1d2e-8a3b-4c5d-9e6f-1a2b3c4d5e6f",
  "event": "license.activated",
  "created_at": "2026-10-03T12:00:00+00:00",
  "api_version": "v1",
  "data": {
    "license": { "id": "01J8Z...", "status": "active", "product": "MYCRM" },
    "customer": { "id": "01J8Y...", "email": "[email protected]", "name": "Northwind Traders" },
    "activation": { "domain": "example.com", "installation_id": "inst_8f72d91c..." }
  }
}

Verifying the signature

Three headers accompany every delivery:

X-Webhook-Id:        0f7c1d2e-8a3b-4c5d-9e6f-1a2b3c4d5e6f
X-Webhook-Event:     license.activated
X-Webhook-Timestamp: 1791019200
X-Webhook-Signature: sha256=<hex hmac>

The signed payload is the timestamp, a full stop, then the raw request body:

$signature = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);

if (! hash_equals($signature, $header)) {
    http_response_code(401);
    exit;
}

Binding the timestamp prevents a captured delivery being replayed later. Reject anything outside a five-minute window.

Always compare the raw body — re-encoding the JSON changes the bytes and breaks verification.

The signing secret is shown once when the endpoint is created and is encrypted at rest afterwards. Rotate it from the admin screen if it leaks.

Retries

Attempt Delay
1 immediate
2 1 minute
3 5 minutes
4 15 minutes
5 1 hour
6 6 hours

After the last attempt the delivery is marked failed and stays visible in the admin UI. It is not retried indefinitely — return a 2xx as soon as you have persisted the event, and process it asynchronously.

A response is treated as success on any 2xx.

Example receiver

$raw     = file_get_contents('php://input');
$event   = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';
$secret  = getenv('WEBHOOK_SECRET');
$ts      = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';

if (abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}

$expected = 'sha256='.hash_hmac('sha256', $ts.'.'.$raw, $secret);

if (! hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

// Acknowledge quickly; process afterwards so a slow handler is not retried.
$id = $_SERVER['HTTP_X_WEBHOOK_ID'];
file_put_contents('/tmp/webhook-queue', $id."\n", FILE_APPEND);

http_response_code(204);

Deduplication

X-Webhook-Id is stable across retries for the same delivery. Record it and ignore repeats — a retried delivery is not a new event.

Page 6 of 8