Документация
FRANKLAB API

Вебхуки

Push-события биллинга и задач с подписью HMAC-SHA256.

Открыть .md
Открыть в AI

Разобраться с AI-ассистентом

Передаётся документация выбранного раздела, без ваших ключей. Большие разделы копируются целиком для вставки в чат.

Cursor и Claude Code откроются с готовым текстом, если он помещается в ссылку. ChatGPT и Antigravity: вставьте скопированный контекст вручную.

Задать вопрос

Вебхуки — push-уведомления о событиях биллинга и задач. Регистрируйте HTTPS-эндпоинт, подписывайтесь на типы событий — и получайте POST-запросы с подписью HMAC-SHA256.

Быстрый старт

Замените https://example.com/franklab-hook на свой публичный HTTPS-обработчик. Сохраните endpoint.id и секрет из ответа регистрации; вместо <id> в следующем запросе подставьте полученный идентификатор.

bash
# зарегистрировать эндпоинт (секрет показывается один раз!)
curl -X POST -H "X-API-Key: $FRANKLAB_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/franklab-hook","eventTypes":["billing.settled","task.succeeded"]}' \
  https://apergrex.ru/franklab/api/v1/webhooks/endpoints

# тестовая доставка
curl -X POST -H "X-API-Key: $FRANKLAB_KEY" \
  https://apergrex.ru/franklab/api/v1/webhooks/endpoints/<id>/test

Типы событий

СобытиеКогда
billing.settledподтверждено списание (с финальной суммой)
billing.refundedвозврат резерва при неудаче задачи
billing.topup.confirmedзачислено пополнение
billing.low_balanceбаланс ниже 1 000 ₣ — тот же порог, что isLow и lowBalanceThresholdFranks в GET /v1/billing/balance (не чаще раза в 24 ч)
billing.invoice.status_changedсчёт юрлица сменил статус/оплачен
task.succeeded / task.failed / task.refundedтерминальный статус задачи генерации
webhook.testтестовое событие (по команде)

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

Заголовки:

Text
X-Franklab-Event: billing.settled
X-Franklab-Event-Id: <uuid события — ключ идемпотентности>
X-Franklab-Timestamp: <unix-секунды>
X-Franklab-Signature: sha256=<HMAC-SHA256(secret, "{timestamp}.{body}")>
X-Franklab-Signature-Previous: sha256=<HMAC прежним секретом — только 6 ч после ротации>

Тело — общий конверт:

json
{
  "eventId": "…",
  "eventType": "billing.settled",
  "occurredAt": "2026-09-04T12:00:00.000Z",
  "partnerId": "…",
  "schemaVersion": 1,
  "data": { "transactionId": "…", "amountFranks": 84 }
}

Проверка подписи

js
import { createHmac, timingSafeEqual } from 'node:crypto';

// headers: lowercase header names; rawBody: the original request Buffer.
export function verifyWebhook(secret, headers, rawBody, nowMs = Date.now()) {
  const timestamp = headers['x-franklab-timestamp'];
  if (typeof secret !== 'string' || !secret || !Buffer.isBuffer(rawBody)) return false;
  if (typeof timestamp !== 'string' || !/^[0-9]{10,}$/.test(timestamp)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || !Number.isFinite(nowMs)) return false;
  if (Math.abs(nowMs / 1000 - seconds) > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  return ['x-franklab-signature', 'x-franklab-signature-previous'].some((name) => {
    const signature = headers[name];
    if (typeof signature !== 'string' || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
    const received = Buffer.from(signature.slice(7), 'hex');
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
}

Передавайте исходные байты тела до JSON-разбора (например, express.raw({ type: 'application/json' }) на маршруте вебхука до express.json()). JSON.stringify(req.body) не заменяет исходное тело. Секрет храните на сервере; во время перехода можно проверить текущую или предыдущую подпись известным вам секретом.

После проверки подписи разберите JSON и надёжно сохраните eventId с событием. Уникальный eventId и запись события должны сохраняться атомарно: повторное событие возвращает 2xx без повторного бизнес-действия. Подтверждайте 2xx после надёжного приёма, тяжёлую обработку выполняйте отдельно. Если сохранить событие не удалось, не отвечайте успешным кодом. Проверка подписи сама по себе не заменяет дедупликацию.

Ротация секрета и grace-окно

После POST /v1/webhooks/endpoints/{id}/rotate-secret доставки в течение 6 часов несут две подписи: X-Franklab-Signature — новым секретом, X-Franklab-Signature-Previous — прежним. Проверяйте любую из них: если верна хотя бы одна, событие подлинно. Окно в 6 часов не покрывает всю цепочку повторов: сумма задержек составляет 8 ч 36 мин, не считая обработки и ожидания очереди. Получатель должен перейти на новый секрет до окончания окна. По истечении окна заголовок -Previous исчезает, а прежний секрет затирается на сервере.

Гарантии доставки

  • At-least-once: дубликаты возможны — идемпотентируйте по eventId.
  • eventId событий из источников (биллинг, задачи, счета) — детерминированный uuid: повторная доставка того же бизнес-события приходит с тем же eventId, так что дедупликация по нему полная. Исключение — billing.low_balance (лимитированная повторная эмиссия раз в 24 ч) и webhook.test: у них каждый выпуск — новое событие с новым eventId.
  • Дубликаты возможны и после рестарта поллера — запасной ключ дедупликации: eventType + data.transactionId / data.taskId / data.invoiceId.
  • Задержка — до минуты (поллер).
  • Повторы при неудаче: 1 мин → 5 мин → 30 мин → 2 ч → 6 ч, затем dead-letter (виден в GET /v1/webhooks/deliveries?status=dead_lettered).
  • Статус uncertain — запрос отправлен, но исход неизвестен: такие доставки не повторяются автоматически. Сначала сверьте обработку на получателе по eventId, затем при необходимости повторите через POST /v1/webhooks/deliveries/{id}/replay.
  • Отвечайте 2xx быстро; таймаут — 10 секунд.

Двойная ротация в окне 6 ч: хранится только один предыдущий секрет — при второй ротации подпись поколением N−2 умирает немедленно; переключайте приёмник на новый секрет без промедления.

Управление

ОперацияЭндпоинт
СписокGET /v1/webhooks/endpoints
РегистрацияPOST /v1/webhooks/endpoints
УдалениеDELETE /v1/webhooks/endpoints/{id}
Ротация секретаPOST /v1/webhooks/endpoints/{id}/rotate-secret
ТестPOST /v1/webhooks/endpoints/{id}/test
История доставокGET /v1/webhooks/deliveries?endpointId=…&status=…
Повтор доставкиPOST /v1/webhooks/deliveries/{id}/replay

Ограничения: до 10 эндпоинтов на партнёра; URL — только публичный HTTPS (внутренние адреса и IP-литералы отклоняются); редиректы не следуются.

/docs/webhooks.md
Не нашли ответ?Связаться с поддержкой