CertifiEd

Webhooks

CertifiEd notifies external systems about license events through outgoing webhooks. Delivery is performed by the WebhookDispatchWorker background worker — with retries and an HMAC signature over the request body.

Endpoint management is operator-only, through the panel API at /api/v1/panel/webhooks — see the API reference.

Events

The live list comes from GET /api/v1/panel/webhooks/event-types (sourced from WebhookEventTypes).

EventPublished whenKey `data` fields
license.issuedA license has been issuedlicenseId, licenseKey, companyId, templateId, expiresAt
license.activatedFirst activation on a new machinelicenseId, licenseKey, activationId, hwFingerprint, machineName, clientIp
license.deactivatedAn activation was droppedlicenseId, licenseKey, activationId
license.revokedA license was revokedlicenseId, licenseKey, reason, revokedAt
license.expiredA license expireddepends on the event source
license.heartbeat_missedAn expected heartbeat was misseddepends on the event source

An endpoint subscribes to a subset of events (events); deliveries are only created for active (isActive) endpoints subscribed to that type.

Delivery format

A POST to the endpoint URL; the body is a JSON envelope:

JSON
{  "event": "license.issued",  "occurredAt": "2026-07-16T14:06:59.1234567+00:00",  "data": {    "licenseId": "019f6b40-89c4-7d41-be2a-4a151a3c0063",    "licenseKey": "CFED-S7FN-RJFA-KAEY-8KZC",    "companyId": "019f6b40-0f9c-7efd-996b-0dadd2ed1077",    "templateId": "019f6b40-0fb8-78f0-9af2-e3589de54069",    "expiresAt": "2027-07-16T14:06:59+00:00"  }}
HeaderValue
Content-Typeapplication/json
X-CertifiEd-EventThe event type, e.g. license.issued
X-CertifiEd-DeliveryDelivery GUID — use it for idempotency on the receiver
X-CertifiEd-Signaturesha256={hex} — HMAC-SHA256 of the body

The X-CertifiEd-Signature header

text
X-CertifiEd-Signature: sha256=<hex( HMAC_SHA256(key = endpoint.secret, msg = raw_body) )>
  • Key — the endpoint secret (secret, at least 16 characters) set at creation time.
  • Message — the raw request body bytes (UTF-8) exactly as received: do not reformat the JSON before verifying.
  • Value — lowercase hex prefixed with sha256=.

Receiver-side verification — C# (ASP.NET Core)

C#
[HttpPost("/webhooks/certified")]public async Task<IActionResult> Receive(){    // 1. Raw body — before any deserialization.    using var reader = new StreamReader(Request.Body);    var body = await reader.ReadToEndAsync();
    // 2. Expected signature.    var expected = "sha256=" + Convert.ToHexStringLower(        System.Security.Cryptography.HMACSHA256.HashData(            Encoding.UTF8.GetBytes(secret),       // the endpoint secret            Encoding.UTF8.GetBytes(body)));
    var received = Request.Headers["X-CertifiEd-Signature"].ToString();
    // 3. Constant-time comparison.    var ok = CryptographicOperations.FixedTimeEquals(        Encoding.ASCII.GetBytes(expected),        Encoding.ASCII.GetBytes(received));    if (!ok)        return Unauthorized();
    // 4. Idempotency by X-CertifiEd-Delivery, then handle the event.    var deliveryId = Request.Headers["X-CertifiEd-Delivery"].ToString();    // ... dedupe(deliveryId); handle(body);    return Ok();}

Receiver-side verification — Node.js (Express)

JavaScript
const crypto = require('crypto');
// Raw body required: express.raw({ type: 'application/json' })app.post('/webhooks/certified', express.raw({ type: 'application/json' }), (req, res) => {  const body = req.body;                        // Buffer with the raw bytes  const expected = 'sha256=' + crypto    .createHmac('sha256', secret)               // the endpoint secret    .update(body)    .digest('hex');
  const received = req.get('X-CertifiEd-Signature') || '';  const a = Buffer.from(expected);  const b = Buffer.from(received);  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {    return res.status(401).end();  }
  const deliveryId = req.get('X-CertifiEd-Delivery');  const payload = JSON.parse(body.toString('utf8'));  // ... dedupe(deliveryId); handle(payload);  res.status(200).end();});

Retry policy

The worker polls the queue every 15 seconds and sends due deliveries. Success means 2xx; any other response or a network error triggers a retry.

AttemptDelay before the next one
1+1 minute
2+5 minutes
3+30 minutes
4+2 hours
5+12 hours
after 5status Failed, retries stop

The delivery HTTP timeout is 30 seconds. The receiver's response body is stored (up to 2000 characters) in responseBody, the status code in responseStatusCode and the last error in lastError.

Replay (manual re-send)

text
POST /api/v1/panel/webhooks/deliveries/{deliveryId}/replay

This resets the delivery: attemptCount → 0, status → Pending, nextAttemptAt → now. On the next cycle the worker sends it again — handy after fixing the receiver or while testing signature verification. The endpoint's delivery history is at GET /api/v1/panel/webhooks/{id}/deliveries.