Вебхуки
Zapnoty отправляет HTTP POST на ваш URL при наступлении событий. Один вебхук на проект. Настраивается в дашборде или через API.
Как настроить вебхук
Вебхуки настраиваются как независимые эндпоинты — до 5 на проект. У каждого свой URL, свой набор событий и свой секрет подписи. Создать эндпоинт можно через API или во вкладке «Вебхук» в кабинете проекта — оба способа работают с одними и теми же эндпоинтами.
Создание эндпоинта — POST /v1/webhooks. Поля: url (обязательно), events (список событий; пустой массив — подписка на все события), description (опциональная заметка).
Управление существующими эндпоинтами — GET для списка, PUT для изменения (url, events, description, status active/disabled), DELETE для удаления. Секрет можно ротировать без простоя: POST /v1/webhooks/{id}/rotate-secret выдаёт новый секрет, старый остаётся валидным ещё 24 часа.
Поле secret возвращается только один раз — при создании эндпоинта и при ротации. Сохраните его сразу: повторно получить секрет нельзя, только ротировать.
Формат доставки
Webhook приходит POST-запросом. Тело всегда — объект { event, data }: event — имя события, data — полезная нагрузка события. Метаданные (timestamp, delivery_id, имя события, подпись) передаются HTTP-заголовками, не в теле.
X-Zapnoty-Delivery-Id одинаков для всех повторных попыток одной доставки — используйте его для дедупликации.
Проверка подписи
Заголовок X-Zapnoty-Signature имеет формат t=<unix_timestamp>,v1=<hex>. Подписывается строка «timestamp + точка + сырое тело запроса»:
signed_payload = "<timestamp>" + "." + raw_body— hex-подпись в нижнем регистре- Подписывайте СЫРОЕ тело запроса, а не результат JSON.parse → JSON.stringify (порядок ключей/пробелы изменятся).
- timestamp для проверки берите из t= ВНУТРИ X-Zapnoty-Signature — именно с этим значением считалась подпись. Отдельный заголовок X-Zapnoty-Timestamp дублирует его лишь для удобства, для верификации используйте t=.
- При ротации секрета (grace-период) приходит несколько v1= — подходит совпадение с любой.
⚠️ timestamp в подписи — только для целостности, НЕ replay-окно. Webhook может прийти повторной попыткой спустя часы (ретраи — до 24 ч), поэтому НЕ отклоняйте «устаревшие» webhook по timestamp — иначе потеряете доставки. Для защиты от повторной обработки используйте дедупликацию по X-Zapnoty-Delivery-Id (он одинаков для всех попыток одной доставки).
События
При настройке webhook-эндпоинта можно подписаться на конкретные события или на все сразу (пустой список). Полный набор:
- subscription.created / .deleted / .permission_changed
- delivery.success / delivery.failed
- broadcast.completed / .cancelled · batch.completed
- otp.sent / .verified / .failed_attempt / .max_attempts_reached
- auth.completed · button.clicked
- ticket.created / .replied / .status_changed / .assigned / .closed / .csat_received / .unanswered_24h
- scheduled.sent / .failed / .skipped · drip.step_sent / .completed · recurring.sent
- event.tracked · form.submitted
Подписки
subscription.created, subscription.deleted и auth.completed несут одинаковый набор полей подписчика. subscriber_id — стабильный UUID (без префикса), единственный канонический идентификатор.
- subscription.created — пользователь подписался любым путём (deep-link, Mini App, авторизация и т.п.).
- auth.completed — завершилась именно auth-сессия (/v1/auth/session); только это событие содержит ваш state и auth_time. Для привязки subscriber_id к своему user_id слушайте auth.completed.
Недоставка — delivery.failed
Поле reason — машиночитаемая причина. retryable — стоит ли повторять отправку. should_unsubscribe — нужно ли убрать получателя из аудитории (используйте этот флаг вместо разбора текста error).
user_blocked_bot — Пользователь заблокировал бота. should_unsubscribe: true
chat_not_found — Чат не найден / аккаунт удалён. should_unsubscribe: true
bot_banned — Бот забанен платформой. retryable: false
invalid_message — Невалидный payload сообщения (медиа, разметка). retryable: false
rate_limited — Лимит платформы (429). retryable: true
platform_unavailable — 5xx / сетевая ошибка платформы. retryable: true
unknown — Неклассифицированная ошибка — нужен ручной разбор.
Завершение рассылки — broadcast.completed
Приходит, когда массовая рассылка (/v1/broadcast) полностью обработана. job_id совпадает с тем, что вернул POST /v1/broadcast. sent — успешно доставлено, failed — ошибки доставки, skipped — пропущено (неактивные/отписавшиеся), total — сумма всех трёх. Событие broadcast.cancelled несёт те же поля для отменённой рассылки.
Политика ретраев
При ошибке доставки webhook автоматически повторяется до 3 раз с exponential backoff (1с → 2с → 4с). Таймаут каждой попытки — 10 секунд. При 4xx от вашего сервера повторы не выполняются. Рекомендуем реализовать идемпотентную обработку на своей стороне.