Приймайте вебхуки Slen
PHPПеревірте підпис доставки, прочитайте її як типізовану подію, відповідайте Slen і відрізняйте доставку від її повтору.
5 хв читання
Slen повідомляє вашому сервісу, що сталося, — запуск закінчився, кредити клієнта закінчуються, погодження запитали чи щодо нього вирішили — вебхуком: POST на вашу адресу, підписаний секретом цього вебхука. SDK перевіряє підпис і перетворює доставку на типізовану подію. API-ключ для цього йому не потрібен, і запитів він не надсилає.
Перш ніж почати
Скопіюйте його секрет підпису — він показується один раз — і зберігайте його разом із сервісом — Збережіть секрет.
Прийняти доставку
Доставка — це POST, тіло якого — подія. Її підпис іде в заголовку поруч із типом тіла:
POST /webhooks/slen HTTP/1.1
Content-Type: application/json
Slen-Signature: t=1792233600,v1=5f0c8e2a…t — час підпису, а v1 — підпис. Приклад сам підписує тіло — замість запиту, який Slen надсилає вашому приймачеві.
// 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);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ви вже бачили.
Відхилена доставка
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 пише відмову у свій журнал, із причиною й віком підпису — але ніколи з тілом, заголовком чи секретом:
[
{ "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, не читаючи подію.
Що читати далі
Вебхуки — що несе доставка, як робиться її підпис, і кожна подія.
Клієнти та кредити — поріг, який надсилає
customer.balance_low.Асинхронні запуски й очікування —
execution.finishedзамість очікування.