События
Вебхуки
Как APIStend доставляет события и чем профили Bitrix24, Ozon, Wildberries и Apify отличаются друг от друга.
На этой странице · 8
Вебхук в APIStend — это подписка «событие сервиса → ваш обработчик». Стенд собирает тело события в родном для сервиса формате, отправляет его и записывает исход в журнал доставок.
Главное, что нужно понять до всего остального: политика доставки принадлежит сервису, а не транспорту. Боевой Bitrix24 не повторяет доставку вообще, Ozon повторяет по своей лестнице и ставит подписку на паузу, Wildberries повторяет по своей и в конце удаляет событие. Единая сетка повторов на всех трёх дала бы ложную картину ровно в том сценарии, ради которого стенд и нужен: «что будет, если мой обработчик упал».
Как проходит доставка
- 1
Событие попадает в очередь
Строка доставки пишется в базу в состоянии
queued— до попытки отправки. Онлайн- и офлайн-путь совпадают: если получателя сейчас нет, строка просто останется в очереди. - 2
Стенд пытается отдать её немедленно
Для локального получателя — кадром в WebSocket агента
apistend listen, для публичного — обычным HTTP-запросом с сервера APIStend. - 3
Приходит ответ, стенд применяет правило успеха своего сервиса
Успех —
succeeded. Неуспех — либоqueuedс временем следующей попытки, либо терминальное состояние, если повторы у сервиса кончились или их нет. - 4
Планировщик подбирает просроченные повторы
Он же переводит зависшие доставки в
no_response. Проход — раз в две секунды, до 50 доставок за проход.
Чем отличаются три профиля
Это главная таблица раздела. Значения — из packages/shared/src/services.ts,
профиль сервиса единственное место, где живут эти отличия.
Про таймауты и повторы стоит помнить, откуда они взялись: у Ozon превышение пяти секунд само по себе одно из условий автоматической приостановки уведомлений, у Wildberries обработчик обязан ответить 200 в течение десяти секунд, а Bitrix24 про повторы говорит дословно: событие отправляется один раз, сбой фиксируется, повторной отправки нет.
Тело события
Тело собирается в родном формате сервиса. Если оно отличается от боевого, разработчик напишет разбор, который в проде не заработает, — поэтому это самое чувствительное место всей функции.
Форма, а не JSON, вложенность — PHP-скобками. В событии CRM лежит только
идентификатор объекта: значения полей не передаются, обработчик обязан сходить
за ними в crm.deal.get.
event=ONCRMDEALUPDATE
event_handler_id=975
data[FIELDS][ID]=7445
ts=1788948929
auth[domain]=demo.bitrix24.ru
auth[client_endpoint]=https://demo.bitrix24.ru/rest/
auth[server_endpoint]=https://oauth.bitrix24.tech/rest/
auth[member_id]=d897063e1ce7c5eb9f04b9751eef5915
auth[application_token]=stend_whsec_…
(в запросе это одна строка application/x-www-form-urlencoded; здесь она разбита
по параметрам для чтения)
Плоский объект, единого конверта нет: дискриминатор message_type лежит рядом
с полезными полями.
{
"message_type": "TYPE_NEW_POSTING",
"posting_number": "24219503-0023-1",
"products": [{ "sku": 1600000411, "quantity": 1 }],
"in_process_at": "2026-09-09T10:15:54Z",
"warehouse_id": 1020000100003,
"seller_id": 12345
}
Конверт со списком событий, у каждого — свой idempotencyKey.
{
"sellerId": "12345",
"requestId": "f4fd4806-9275-4aeb-85e2-8ef5cd807a65",
"events": [
{
"event": "stocks_changed",
"warehouseId": 507170,
"nmId": 194873070,
"quantity": 90,
"updatedAt": "2026-09-09T10:15:54Z",
"idempotencyKey": "idm-236e"
}
]
}
Конверт Wildberries всегда с одним событием
В профиле сервиса записано, что боевой WB кладёт в один запрос до 100 событий.
Стенд эту пачку пока не собирает: каждая доставка — свой запрос с массивом
events из одного элемента. Разбор писать всё равно нужно по массиву, иначе
на бою он сломается на первой же пачке.
События, отправленные кнопкой «Тест» и командой apistend trigger без серии,
помечаются: у Wildberries в объект события добавляется "test": true.
Заголовки доставки
Подпись Wildberries считается от сырого тела запроса ключом самого вебхука
(secret подписки, значение вида stend_whsec_…), результат — hex в нижнем
регистре. Проверять её нужно до разбора JSON: пересобранное тело даст другую
подпись.
import { createHmac, timingSafeEqual } from 'node:crypto'
const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
const got = req.headers['x-hub-signature']
const ok = expected.length === got.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(got))
У Bitrix24 подписи в заголовке нет — вместо неё в теле приходит
auth[application_token], его и сравнивают. У Ozon подписи нет вовсе, это
свойство боевого сервиса, а не упрощение стенда.
Что считается успехом
Проверку делает сервер: только у него есть тело ответа.
- Bitrix24 — любой код 2xx.
- Wildberries — ровно 200. 201 или 204 не подойдут.
- Ozon — 200 и тело
{"result": true}. Пустое тело, невалидный JSON или{"result": false}считаются неуспехом; в журнале это видно какEMPTY_BODY,INVALID_JSONилиWRONG_RESULT_FIELD. - Ozon, событие
TYPE_PING— ответ должен содержатьversion,nameиtime.
Повторы, приостановка и отключение
Повтор планируется по лестнице сервиса: к времени ответа прибавляется задержка
для текущего номера попытки, доставка возвращается в queued, планировщик
поднимает её, когда срок наступит, и увеличивает счётчик попыток.
Когда повторы кончились, а сервис приостанавливает подписки (Ozon и Wildberries),
вебхук переходит в состояние failing. Успешная доставка его оттуда не выводит —
снятие приостановки только ручное, переключателем в строке вебхука. Так же ведёт
себя боевой Ozon: приостановленная подписка возобновляется только вручную.
Переключатель в интерфейсе выключает вебхук в состояние paused из любого
другого состояния. На паузе одиночная отправка ещё проходит, а серия событий
отказывается стартовать с сообщением «Вебхук на паузе».
Кнопка «Повторить ошибочные» ставит в очередь до 50 доставок в состояниях
failed и no_response — кроме доставок Bitrix24: у боевого сервиса повторов
нет, и мок не должен быть добрее.
Публичный получатель
Кроме доставки на localhost вебхук можно направить на обычный публичный адрес — тогда запрос делает сам сервер APIStend. Из-за этого адрес проверяется, причём дважды: при создании подписки и ещё раз перед каждой отправкой, включая повторы и события серии.
Отклоняются приватные и служебные диапазоны: 10/8, 127/8, 172.16/12,
192.168/16, 169.254/16 (метаданные облаков), 100.64/10, IPv6 ::1, fc00::/7,
fe80::/10 и IPv4, завёрнутый в IPv6. Имя резолвится: домен, указывающий на
приватный адрес, тоже не пройдёт. Такая доставка сразу получает терминальное
состояние failed с причиной blocked — ждать нечего, адрес не станет публичным.
В разработке приватные адреса разрешены
Переключатель WEBHOOK_ALLOW_PRIVATE_TARGETS снимает проверку целиком: на локальной
машине API и приложение живут рядом. Проверка от перепривязки DNS между
проверкой и запросом не спасает — это известное ограничение, а не недосмотр.
Подводные камни
- Тело Bitrix24 — не JSON.
JSON.parseна нём упадёт. Разбирайтеapplication/x-www-form-urlencodedи помните про PHP-скобки в именах параметров. - Ozon смотрит в тело. Обработчик, который отвечает
200 OKпустым телом, на Ozon считается упавшим и через одиннадцать попыток отправит подписку на паузу. - У Bitrix24 попытка одна. Ошибка в обработчике — событие потеряно навсегда, повторить его может только человек, и то не через «Повторить ошибочные».
- Пауза не снимается сама. После серии неудач вебхук останется в
failing, пока его не включат руками.