Приймайте вебхуки Slen

PHP

Перевірте підпис доставки, прочитайте її як типізовану подію, відповідайте Slen і відрізняйте доставку від її повтору.

5 хв читання

Slen повідомляє вашому сервісу, що сталося, — запуск закінчився, кредити клієнта закінчуються, погодження запитали чи щодо нього вирішили — вебхуком: POST на вашу адресу, підписаний секретом цього вебхука. SDK перевіряє підпис і перетворює доставку на типізовану подію. API-ключ для цього йому не потрібен, і запитів він не надсилає.

Перш ніж почати

  • Додайте вебхук на сторінці Вебхуки: адресу вашого приймача й події, які він слухає, — покроково це описано в статті Вебхуки.

  • Скопіюйте його секрет підпису — він показується один раз — і зберігайте його разом із сервісом — Збережіть секрет.

Прийняти доставку

Доставка — це POST, тіло якого — подія. Її підпис іде в заголовку поруч із типом тіла:

HTTP
POST /webhooks/slen HTTP/1.1
Content-Type: application/json
Slen-Signature: t=1792233600,v1=5f0c8e2a…

t — час підпису, а v1 — підпис. Приклад сам підписує тіло — замість запиту, який Slen надсилає вашому приймачеві.

PHP
// Stands in for the request Slen sends to your endpoint: its raw body and its
// Slen-Signature header, signed here with the secret of the example's endpoint.
$secret = 'whsec_example_endpoint';
$payload = '{"id":"74438200973594801","type":"execution.finished","created_at":"2026-10-17T12:00:00+00:00",'
    . '"data":{"execution_id":"74438200973594700","flow_id":"74438200973590001","operation":"generate_email","status":"completed","error_code":null}}';
$timestamp = time();
$signature = 't=' . $timestamp . ',v1=' . hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
PHP
use Slen\Sdk\Modules\Webhooks\Data\CustomerBalanceLowEvent;
use Slen\Sdk\Modules\Webhooks\Data\ExecutionFinishedEvent;
use Slen\Sdk\Modules\Webhooks\Options\WebhooksOptions;
use Slen\Sdk\Modules\Webhooks\WebhooksFacade;

// A receiver needs the secret of the endpoint, not an API key. Pass the body as it arrived.
$webhooks = WebhooksFacade::standalone(new WebhooksOptions(secret: $secret));

$event = $webhooks->constructEvent($payload, $signature);

if ($event instanceof ExecutionFinishedEvent) {
    echo $event->type, ': ', $event->executionId, ' ', $event->status->value, PHP_EOL;
}

if ($event instanceof CustomerBalanceLowEvent) {
    echo $event->type, ': ', $event->externalUserId, PHP_EOL;
}
  • Передавайте тіло рівно таким, яким воно прийшло, байт у байт. Тіло, яке ваш фреймворк прочитав як JSON і записав наново, — не те тіло, яке підписав Slen, і перевірка його відхиляє.

  • Секрет, переданий у виклик, має пріоритет: secret його опцій, VerifyOptions. Без нього перевірка бере секрет, із яким зібрано SDK, — webhookSecret. Якщо немає ні того, ні іншого, виклик відхиляється, перш ніж щось перевірено.

Беріть тіло таким, яким воно прийшло: $request->getContent() у Laravel. WebhooksFacade::standalone() дає приймач, якому не потрібен API-ключ: секрет, допуск і логер несуть його WebhooksOptions. $slen->webhooks() — та сама перевірка із секретом і допуском із конфігурації SDK.

Події

Чотири події мають власний тип, із прочитаними й типізованими полями: execution.finished, customer.balance_low, approval.requested і approval.decided. Будь-яка інша подія приходить зі своїм type і своїм data — такими, якими їх надіслав Slen, — і так само приходить подія, новіша за SDK. Кожну подію й поля цих чотирьох перелічує стаття Вебхуки.

Подія — це ExecutionFinishedEvent, CustomerBalanceLowEvent, ApprovalRequestedEvent або ApprovalDecidedEvent, кожна — зі своїми полями як властивостями. Будь-яка інша подія — UnknownWebhookEvent, із type і сирим data.

Відповісти на доставку

  • Відповідайте 2xx, щойно підпис зійшовся, а роботу робіть потім. Доставку, яка не отримала 2xx вчасно, надсилають ще раз — коли й скільки разів, каже стаття Вебхуки.

  • Кожна доставка однієї події несе той самий id. Зберігайте ідентифікатори, які ваш приймач уже обробив, і пропускайте доставку, чий id ви вже бачили.

Відхилена доставка

PHP
use Slen\Sdk\Exceptions\WebhookSignatureException;

try {
    $webhooks->constructEvent(str_replace('completed', 'failed', $payload), $signature);
} catch (WebhookSignatureException $refused) {
    // Not the body Slen signed: answer 400 and do nothing with it.
    echo 'refused: ', $refused->reason, PHP_EOL;
}

Якщо підпис не зійшовся, кидається WebhookSignatureError у Node.js і Python і WebhookSignatureException у PHP, із кодом webhook_signature_invalid і полем reason. Відповідайте 400 і нічого не робіть із тілом. SDK пише відмову у свій журнал, із причиною й віком підпису — але ніколи з тілом, заголовком чи секретом:

slen.webhook.rejected
[
  { "reason": "missing_header", "age_seconds": null },
  { "reason": "malformed_header", "age_seconds": null },
  { "reason": "outside_tolerance", "age_seconds": 412 },
  { "reason": "no_matching_signature", "age_seconds": 2 }
]
ПричинаЧому
missing_headerЗапит не має заголовка підпису, або він порожній
malformed_headerУ заголовку немає часу t або немає підпису v1
outside_toleranceПідпис зроблено раніше, ніж дозволяє webhookToleranceSeconds, — за замовчуванням 300 секунд, — або годинник вашого сервера йде неправильно
no_matching_signatureЖоден підпис заголовка не збігається з тілом: його підписав інший секрет, або тіло змінили
  • Заголовок може нести кілька підписів, і досить одного, що збігся.

  • toleranceSeconds із VerifyOptions замінює допуск для одного виклику, а now — момент, на який перевіряється підпис.

  • Для тіла, підпис якого зійшовся, але яке не є подією, кидається InvalidResponseError або InvalidResponseException, із кодом webhook_payload_invalid.

  • webhooks.verify() робить ту саму перевірку й відповідає true або false, не читаючи подію.

Що читати далі