Перейти до основного вмісту

Вебхуки

Вебхук доставляє кожну завершену перевірку на ваш сервер, тож опитувати нічого не потрібно. Ви один раз реєструєте HTTPS-кінцеву точку, і відтоді кожна перевірка, що досягла завершального стану, надсилається на неї підписаним POST. Опитування GET /v1/checks/{check_id} залишається запасним варіантом.

Що доставляється​

Є один тип події, check.completed. Її надсилають один раз для кожної перевірки, що досягла завершального стану, completed або failed, зокрема для кожного елемента пакета (одна доставка на елемент, а не на пакет). Перевірка в стані pending нічого не надсилає.

Подію отримує кожна активна кінцева точка середовища, і кожна кінцева точка отримує власну доставку. Кінцеві точки ключа sandbox отримують лише події sandbox, а кінцеві точки live-ключа лише події live; поле environment каже, яка з них.

Тіло — JSON:

ПолеОпис
event_idУнікальний ідентифікатор події. Той самий ідентифікатор надсилається знову при кожній повторній спробі тієї самої доставки: дедуплікуйте за ним.
event_typecheck.completed.
environmentlive або sandbox.
created_atКоли подію створено, мітка часу ISO 8601. Однакова при кожній повторній спробі.
checkПовна перевірка з тими самими полями, що й data у відповіді GET /v1/checks/{check_id}; див. Перевірки адрес.
{
"event_id": "9d2f6c1a-3b7e-4f08-a5d4-1c8e0b7a6f23",
"event_type": "check.completed",
"environment": "live",
"created_at": "2026-10-27T09:14:04.931Z",
"check": {
"check_id": "5b0c3f64-1a52-4c8e-9f3e-2f6c1d7a9e10",
"status": "completed",
"band": "high",
"score": 74
}
}

У прикладі check скорочено до кількох полів; доставляється повний об'єкт перевірки. Сприймайте доставку як той самий запис, який ви отримали б, прочитавши перевірку: вердикт не змінюється після того, як став завершальним.

Зареєструйте кінцеву точку​

POST /v1/webhooks зі scope webhooks:manage.

ПолеОбов'язковеОпис
urlтакHTTPS-адреса, що отримує доставки, не більше 2048 символів. Невідомі поля відхиляються.

URL має використовувати https. Його хост розв'язується під час реєстрації, а хост, що розв'язується в адресу з приватного чи іншого зарезервованого діапазону, відхиляється з 400. Та сама перевірка виконується перед кожною доставкою, бо DNS можуть переспрямувати після реєстрації. Якщо хост розв'язати не вдалося, запит повертає 503. Кожне середовище може мати до 20 активних кінцевих точок; 21-шу відхилено з 400.

curl -s -X POST https://api.leeri.ai/v1/webhooks \
-H "Authorization: Bearer $LEERI_API_KEY" \
-H "Idempotency-Key: register-orders-endpoint" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-service.example.com/webhooks/leeri"}'

Відповідь 201 містить кінцеву точку:

ПолеОпис
webhook_idІдентифікатор кінцевої точки, потрібний, щоб ротувати чи вимкнути її.
urlЗареєстрований URL.
secretСекрет підпису. Показується один раз, лише в цій відповіді. Збережіть його у своєму сховищі секретів зараз: він не зберігається у читабельному вигляді й більше не повертається, навіть коли той самий запит повторюють із тим самим Idempotency-Key.
secret_last4Останні чотири символи секрету, щоб розрізняти секрети.
statusactive або disabled після вимкнення.
previous_secret_expires_atnull або кінець вікна ротації; див. Ротація секрету.
rotated_at, created_at, updated_atМітки часу.

Якщо ви втратили секрет, зробіть ротацію кінцевої точки, щоб отримати новий.

Перегляд, ротація й вимкнення​

Усі три дії потребують scope webhooks:manage і приймають необов'язковий Idempotency-Key там, де щось змінюють (див. Ідемпотентність).

  • GET /v1/webhooks показує кінцеві точки середовища ключа, активні й вимкнені, від найстаршої. Перегляд показує secret_last4 і ніколи не показує секрет.
  • POST /v1/webhooks/{webhook_id}/rotate видає новий секрет; див. нижче.
  • DELETE /v1/webhooks/{webhook_id} вимикає кінцеву точку. Відтоді вона нічого не отримує, зокрема й доставки, що ще чекали повторної спроби. Вимкнення вже вимкненої кінцевої точки не є помилкою, а вимкнена кінцева точка більше не враховується в ліміті 20. Невідомий ідентифікатор або ідентифікатор з іншого середовища повертає 404.

Ротація секрету​

Ротація замінює секрет без простою. Протягом 24 годин діють і новий, і попередній секрети: кожну доставку в цьому вікні підписано обома, тож отримувач, що вже перейшов на новий секрет, і той, що ще ні, обидва її перевіряють. previous_secret_expires_at показує, коли вікно закінчується. Новий секрет міститься у відповіді 200, один раз.

Розгорніть новий секрет у своєму отримувачі протягом вікна й після цього припиніть використовувати старий. Ваш код перевірки не змінюється: він уже приймає заголовок із кількома підписами.

Перевірте підпис​

Будь-хто, хто знає ваш URL, може надіслати на нього запит. Кожна справжня доставка несе підпис у заголовку x-aml-signature, а запиту без дійсного підпису не можна довіряти й виконувати за ним дії.

x-aml-signature: t=1793121567192,v1=4abc621d…d9d2
  • t — час підписання доставки в мілісекундах Unix.
  • v1 — HMAC-SHA256 у шістнадцятковому записі, 64 символи. На кожен чинний секрет припадає один v1, тож під час ротації заголовок містить два. Достатньо збігу з будь-яким v1.
  • Підписане повідомлення — рядок <t>.<сире тіло>: значення t, крапка й тіло запиту точно в тому вигляді, в якому воно надійшло. Ключ — секрет вашої кінцевої точки.

Щоб перевірити доставку:

  1. Прочитайте сире тіло до його розбору. Підпис охоплює передані байти, тож тіло, яке розібрали й серіалізували знову, уже не збігається.
  2. Розберіть t і значення v1 із заголовка. Відхиліть запит, якщо t або всі v1 відсутні чи неправильні.
  3. Відхиліть його, якщо t відрізняється від вашого годинника більш ніж на 5 хвилин в будь-який бік. Це не дає повторно відтворити перехоплену доставку пізніше.
  4. Обчисліть HMAC-SHA256 від <t>.<сире тіло> з вашим секретом і порівняйте з кожним v1 порівнянням за сталий час.

Наведені нижче приклади роблять саме це й ніколи не викидають виняток на неправильному заголовку. Їх перевірено на підписах, створених самою службою доставки: правильний підпис, змінене тіло, застаріла мітка часу, два підписи під час ротації та неправильні заголовки.

import crypto from 'node:crypto';

// Pass the raw request body exactly as received, before any JSON parsing: the signature covers
// the transmitted bytes, and re-serialising the parsed object changes them.
export function verifyWebhook(rawBody, header, secret, nowMs = Date.now(), maxSkewMs = 300000) {
if (typeof header !== 'string') return false;

let t = null;
const signatures = [];

for (const part of header.split(',')) {
const eq = part.indexOf('=');
if (eq === -1) continue;
const key = part.slice(0, eq);
const value = part.slice(eq + 1);

if (key === 't' && /^\d+$/.test(value)) t = Number(value);
else if (key === 'v1' && /^[0-9a-fA-F]{64}$/.test(value)) signatures.push(Buffer.from(value, 'hex'));
}

if (t === null || signatures.length === 0) return false;
if (Math.abs(nowMs - t) > maxSkewMs) return false;

const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();

// During a secret rotation the header carries one v1 per valid secret; any match is enough.
return signatures.some((signature) => crypto.timingSafeEqual(signature, expected));
}

// Respond 2xx quickly and process asynchronously. Deliveries are at-least-once, so dedupe on
// the event_id in the payload before acting on it.

Мінімальний отримувач​

Підключіть функцію до маршруту, що зберігає сире тіло, відповідає швидко, а справжню роботу виконує згодом.

import express from 'express';
import { verifyWebhook } from './verify-webhook.mjs';

const app = express();

// express.raw keeps the body as bytes; express.json would parse it first and lose them.
app.post('/webhooks/leeri', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWebhook(req.body, req.get('x-aml-signature'), process.env.LEERI_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}

res.sendStatus(200);

const event = JSON.parse(req.body.toString('utf8'));
queue.add(event); // process later, skipping event.event_id values already handled
});

Обидва отримувачі припускають власну чергу (queue.add, enqueue). Секрет береться зі змінних середовища чи сховища секретів, ніколи не з вихідного коду.

Доставка й повторні спроби​

  • Підтверджуйте будь-яким 2xx протягом 10 секунд. Тіло відповіді не використовується: порожній 204 підходить, а незвично велике тіло вважається збоєм.
  • Перенаправлення не виконуються. Відповідь 3xx вважається невдалою доставкою, тож реєструйте кінцевий URL.
  • Збої, що повторюються: відсутня відповідь або відповідь повільніша за 10 секунд, 408, 429 і кожен 5xx. Тимчасова помилка DNS теж повторюється.
  • Не повторюються: будь-який інший 4xx і будь-який 3xx, бо ваш сервер явно відмовився від доставки, а також URL, що тепер розв'язується в заблоковану адресу. Такі доставки завершуються як невдалі й більше не надсилаються.
  • Розклад повторних спроб: перша повторна спроба негайна, далі через 10 секунд, 1 хвилину й 5 хвилин, потім кожні 10 хвилин, доки доставці менше 24 годин, далі щогодини. Доставку, що досі невдала через 48 годин після створення, відкидають.
  • Принаймні раз. Доставка може надійти більше одного разу, наприклад коли ваш сервер її обробив, але його 2xx загубився. Зберігайте оброблені значення event_id й ігноруйте повтори.
  • Порядок не гарантується. Доставки різних перевірок і повторні спроби можуть надходити не по порядку. Спирайтеся в обробці на check_id, а не на порядок надходження.

Невдала доставка ніколи не впливає на перевірку чи її списання. Якщо ваша кінцева точка не працювала довше, ніж триває наведений розклад, прочитайте пропущені перевірки через GET /v1/checks/{check_id}.

Перевірте на практиці​

Зареєструйте кінцеву точку з ключем sandbox, виконайте перевірку в sandbox і переконайтеся, що ваш отримувач перевіряє підпис і відповідає 2xx. Кінцеві точки sandbox отримують лише події sandbox, тож на вашу live-кінцеву точку нічого не надійде; див. Sandbox і live.