Вебхуки
Вебхук — ваш адрес в интернете, на который лагерь сам отправляет каждое событие вашего
отдела сразу, как оно случилось. Не нужно опрашивать API: оплатил родитель счёт — через секунды ваш
сервер получает invoice.paid.
Что такое события и какие они бывают — в Событиях. Здесь — как завести вебхук,
проверить, что запрос пришёл именно от лагеря, и правильно отвечать. Нужно право webhooks.
Что нужно с вашей стороны
- Адрес вида
https://crm.example.kz/hooks/zvezdnyyна вашем сервере, доступный из интернета. - Только
https://с действующим сертификатом (не самоподписанным). - Публичный адрес. Внутренние адреса (
localhost,10.x.x.x,192.168.x.xи т. п.) не принимаются. - Без логина и пароля в адресе (
https://user:pass@…) и без части после#. Подлинность проверяется подписью, а не паролем. - Адрес должен отвечать сам, без переадресации: ответ
301/302считается неудачей. - Не больше 5 действующих вебхуков на подключение.
Шаг 1. Завести вебхук
POST /webhooks с Idempotency-Key:
Code
event_types— какие типы присылать. Не указывать или пустой список — все типы, включая те, что появятся в будущем. Незнакомый тип — отказ400 bad_event_type.pingуказывать не нужно — он приходит только на проверку.
Ответ 201:
Code
Секрет показывается один раз
secret — ключ для проверки подписи. Он есть только в этом ответе (и в ответе на смену
секрета). Сразу сохраните его в настройках сервера рядом с ключом API. Потеряли — смените секрет
(POST /webhooks/{id}/rotate-secret).
Новый вебхук начинает с текущего конца ленты (delivered_through — номер последнего события на
момент создания): прошлые события на него не придут. Нужна история — заберите её опросом
GET /events или попросите отправить заново (redeliver, ниже).
Шаг 2. Проверить адрес
POST /webhooks/{id}/test — лагерь сразу отправит на адрес событие ping и скажет, что ответил ваш
сервер. Ключ повтора не нужен.
Code
Code
ok: false и status: null — ответа не было вовсе: адрес не найден, нет соединения, плохой
сертификат или сервер не ответил за 10 секунд. Описание — в error.
Что приходит на ваш адрес
Лагерь отправляет POST с одним событием в теле — в том же виде, что в GET /events:
Code
| Заголовок | Что это |
|---|---|
Zb-Event-Id | Номер события — то же, что id в теле |
Zb-Event-Type | Тип события — то же, что type в теле |
Zb-Signature | Подпись: время отправки t и значение v1 |
Шаг 3. Проверять подпись
Ваш адрес — в интернете, на него может написать кто угодно. Проверяйте подпись каждого запроса: только так вы знаете, что событие отправил лагерь и его не подменили по дороге.
-
Разберите заголовок
Zb-SignatureЭто пары
ключ=значениечерез запятую:t=1778752965,v1=5257a869….t— время отправки (секунды Unix).v1может быть несколько — подпись верна, если совпало хотя бы одно. -
Возьмите сырое тело запроса
Байты как пришли, до разбора JSON. Если разобрать JSON и собрать обратно, байты будут другими (пробелы, порядок полей, экранирование кириллицы), и подпись не сойдётся.
-
Посчитайте HMAC-SHA256
Ключ — секрет целиком, вместе с
whsec_. Сообщение — строка<t>.<сырое тело>: время, точка, тело. Результат — в шестнадцатеричном виде (64 символа, строчные буквы). -
Сравните с каждым
v1— сравнением постоянного времени (timingSafeEqual,hmac.compare_digest,hash_equals), а не обычным==. -
Отбросьте запрос, если
tотличается от текущего времени больше чем на 5 минут. Так перехваченный запрос нельзя отправить вам повторно через неделю. Часы вашего сервера должны идти точно (NTP).
Подпись не сошлась — ответьте 400 или 401 и не обрабатывайте событие.
Проверьте свой код на эталоне
Если ваша функция проверки верна, для этих данных она посчитает ровно такой v1:
| Что | Значение |
|---|---|
| Секрет | whsec_test |
t | 1700000000 |
| Тело | {"id":"1"} |
Ожидаемый v1 | 11bf4466ea17c3df3fd743af0b435368e16b7a05eb8eced85e8c4670767bdec5 |
То же в терминале:
Code
(Проверку времени для этого эталона отключите: 1700000000 — это ноябрь 2023 года.)
Шаг 4. Отвечать правильно
- Ответьте
2xx(лучше200) не позже чем за 10 секунд. Не обрабатывайте событие прямо в обработчике: положите его в свою очередь (таблицу, Redis, RabbitMQ) и сразу ответьте. Разбирайте очередь отдельно. - Повтор — тоже
200. Если событие с этимZb-Event-Idвы уже обработали, ответьте200и пропустите его. Иначе лагерь будет повторять его снова и снова. - Незнакомый тип — тоже
200. Пропустите, но не отвечайте ошибкой. - Тело ответа не важно — лагерь его не читает (кроме журнала доставок при ошибке).
Порядок и повторы
- Строго по порядку. На один вебхук события уходят по одному, по возрастанию номера.
Следующее уходит, только когда ваш сервер ответил
2xxна предыдущее. - Это значит: пока вы не ответили
2xxна событие, следующие не придут. Одно событие, на котором ваш обработчик падает, останавливает доставку всех остальных. Поэтому — сначала в очередь и200, потом разбор. - Не ответили
2xx(ошибка, таймаут, переадресация) — повтор через 30 секунд, 2 минуты, 10 минут, 30 минут, 1 час и дальше каждые 2 часа. - Трое суток неудач подряд — вебхук отключается (
enabled: false, причина вdisabled_reason), на почту подключения приходит письмо. - События не теряются. Пока вебхук стоял, лента копилась. Почините сервер и включите вебхук:
PATCH /webhooks/{id}с{"enabled": true}— доставка продолжится с того же места. Или заберите события опросомGET /events.
Управление вебхуком
| Нужно | Запрос |
|---|---|
| Список вебхуков | GET /webhooks |
| Один вебхук и его состояние | GET /webhooks/{id} — failed_attempts, failing_since, next_attempt_at |
| Сменить адрес или типы событий | PATCH /webhooks/{id} с url, event_types |
| Включить снова (сбрасывает счёт неудач) | PATCH /webhooks/{id} с {"enabled": true} |
| Отключить | DELETE /webhooks/{id} — остаётся в списке отключённым, можно включить снова |
| Проверить адрес | POST /webhooks/{id}/test |
| Журнал попыток за 30 дней | GET /webhooks/{id}/deliveries — код ответа вашего сервера, начало ответа, время |
| Отправить заново с нужного места | POST /webhooks/{id}/redeliver с {"after": "1000"} (с Idempotency-Key) |
| Сменить секрет подписи | POST /webhooks/{id}/rotate-secret (с Idempotency-Key) |
Отправить заново (redeliver): всё после номера after уйдёт снова, по порядку, в
ближайшие секунды. Номер меньше уже доставленного — повтор прошлых событий; больше — пропуск
событий до него. События хранятся 90 дней.
Смена секрета
POST /webhooks/{id}/rotate-secret возвращает новый secret — один раз. Старый секрет перестаёт
действовать сразу: события, отправленные после ответа, подписаны новым. Как сменить без потерь:
- Добавьте в настройки сервера место для второго секрета.
- Смените секрет и сохраните новый, не удаляя старый.
- На время перехода принимайте подпись, если она сходится с любым из двух секретов (событие, отправленное за секунду до смены, подписано старым).
- Когда пришло первое событие с новой подписью — удалите старый секрет.
Частые проблемы
| Что видно | Причина |
|---|---|
| Подпись никогда не сходится | Тело разобрано и собрано заново (express.json(), $request->all()), или секрет взят без whsec_ |
| Подпись то сходится, то нет | Часы сервера отстают или спешат больше чем на 5 минут |
| События перестали приходить | Обработчик падает на одном событии и не отвечает 2xx — смотрите GET /webhooks/{id}/deliveries |
| В журнале «Переадресация на …» | Адрес отвечает 301/302 — укажите конечный адрес (часто — https вместо http или слэш в конце) |
test отвечает status: null | Адрес не виден из интернета, закрыт файрволом или сертификат недействителен |