CertifiEd

Webhooks

CertifiEd уведомляет внешние системы о событиях лицензий исходящими webhooks. Доставку выполняет фоновый воркер WebhookDispatchWorker — с ретраями и HMAC-подписью тела запроса.

Управление эндпоинтами доступно только оператору, через panel API /api/v1/panel/webhooks — см. API Reference.

События

Актуальный список отдаёт GET /api/v1/panel/webhooks/event-types (источник — WebhookEventTypes).

СобытиеКогда публикуетсяКлючевые поля `data`
license.issuedВыпущена лицензияlicenseId, licenseKey, companyId, templateId, expiresAt
license.activatedПервая активация на новой машинеlicenseId, licenseKey, activationId, hwFingerprint, machineName, clientIp
license.deactivatedДеактивация активацииlicenseId, licenseKey, activationId
license.revokedЛицензия отозванаlicenseId, licenseKey, reason, revokedAt
license.expiredЛицензия истеклазависит от источника события
license.heartbeat_missedПропущен ожидаемый heartbeatзависит от источника события

Эндпоинт подписывается на подмножество событий (events); доставки создаются только для активных (isActive) эндпоинтов, подписанных на данный тип.

Формат доставки

POST на URL эндпоинта, тело — JSON-конверт:

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"  }}
ЗаголовокЗначение
Content-Typeapplication/json
X-CertifiEd-EventТип события, например license.issued
X-CertifiEd-DeliveryGUID доставки — используйте для идемпотентности на приёмнике
X-CertifiEd-Signaturesha256={hex} — HMAC-SHA256 тела запроса

Подпись X-CertifiEd-Signature

text
X-CertifiEd-Signature: sha256=<hex( HMAC_SHA256(key = endpoint.secret, msg = raw_body) )>
  • Ключ — секрет эндпоинта (secret, минимум 16 символов), заданный при создании.
  • Сообщениесырые байты тела запроса (UTF-8), ровно как пришли: не переформатируйте JSON перед проверкой.
  • Значение — hex в нижнем регистре с префиксом sha256=.

Проверка на приёмнике — 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();}

Проверка на приёмнике — 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-политика

Воркер опрашивает очередь раз в 15 секунд и отправляет due-доставки. Успех — это 2xx; любой другой ответ или сетевая ошибка приводят к ретраю.

ПопыткаЗадержка до следующей
1+1 минута
2+5 минут
3+30 минут
4+2 часа
5+12 часов
после 5статус Failed, ретраи прекращаются

Таймаут HTTP-запроса доставки — 30 секунд. Тело ответа приёмника сохраняется (до 2000 символов) в responseBody, код ответа — в responseStatusCode, последняя ошибка — в lastError.

Replay (ручная переотправка)

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

Сбрасывает доставку: attemptCount → 0, статус → Pending, nextAttemptAt → now. На следующем цикле воркер отправит её заново — полезно после починки приёмника или для проверки подписи. История доставок эндпоинта — GET /api/v1/panel/webhooks/{id}/deliveries.