Что такое вебхук: крючок, который будит другой сервис

Вебхук (webhook) — это пользовательский HTTP-обратный вызов: когда в одной системе происходит событие, она сама отправляет запрос на URL другой системы. По-английски это user-defined HTTP callback. Источник не ждёт, пока его «опросят». Он стучит в момент события и передаёт тело запроса — обычно JSON-payload с типом события, временной меткой и данными объекта.

Коротко: вебхук — это push-модель интеграции. Клиент не тянет данные циклом «есть ли уже новости?», а подписывается на событие и получает POST на собственный endpoint в ту же секунду, когда что-то изменилось.

Именно поэтому HTTP-колбэки стали клеем между CRM, платёжными шлюзами, CI/CD, чатами и no-code платформами. Ниже — не словарная справка, а разбор механики, истории, границ применения, безопасности, типичных сбоев и того, как отличить «настроил за вечер» от задачи для инженера интеграций.

Механизм доставки: кто кого будит и что летит по сети

Классический REST API работает по схеме «спросил — ответил». Вебхук разворачивает стрелку: источник события становится инициатором. GitHub после push в репозиторий, Stripe после успешной оплаты, интернет-магазин после нового заказа — каждый из них формирует сообщение и отправляет его на заранее сохранённый адрес подписчика.

Цепочка выглядит так. Сначала в системе-отправителе регистрируют URL приёмника и перечень событий. Далее, когда событие действительно произошло, отправитель сериализует объект (заказ, платёж, коммит), добавляет заголовки с идентификатором доставки и криптографической подписью и выполняет HTTPS POST. Приёмник должен быстро ответить кодом 2xx — и только после этого браться за тяжёлую работу: записать в базу, отправить письмо, обновить склад.

Анатомия типичного запроса

Метод почти всегда POST. GET встречается в устаревших или упрощённых схемах и плохо подходит для тела с данными. Заголовок Content-Type — application/json. Отдельно идут служебные заголовки: тип события, уникальный ID доставки, подпись HMAC, иногда временная метка. Тело содержит событие, а не «сырой» бизнес-объект в вакууме: есть поле type вроде payment_intent.succeeded или push, время и вложенный data.

Есть два стиля полезной нагрузки. «Толстое» (fat / snapshot) несёт полный снимок объекта на момент события — удобно, но тяжелее и быстрее устаревает, если объект изменится через миллисекунды. «Тонкое» (thin) несёт идентификатор и тип: приёмник сам дотягивает актуальное состояние через API. Stripe даёт оба подхода в зависимости от версии API. Standard Webhooks рекомендует thin-модель именно из-за аудита и согласованности.

Что отправитель считает успехом

Успех — любой 2xx, полученный до таймаута. 3xx с редиректом многие провайдеры не любят: подпись считается на конкретный URL, а прокси по пути может сломать тело. 410 Gone — сигнал «эту подписку можно отключить». 429 Too Many Requests просит снизить темп; корректный отправитель смотрит Retry-After. 5xx и обрыв соединения запускают повторную доставку.

Критическое правило продакшена: сначала сохранить событие в очередь и ответить 200 OK, и только потом обрабатывать. GitHub ждёт ответ около 10 секунд. Shopify — примерно 1 секунду на соединение и 5 секунд на весь запрос. Кто считает скидку, пишет в ERP и шлёт SMS в том же HTTP-запросе, тот гарантированно поймает timeout и дубль.

Повторная доставка — не сбой протокола, а его часть. Stripe в live-режиме ретраит неудачные попытки с экспоненциальной паузой до трёх суток: почти сразу, потом через 5 и 30 минут, 2, 5 и 10 часов, далее примерно каждые 12 часов. Песочница ограничена тремя попытками за несколько часов. Отсюда требование идемпотентности: тот же event.id не должен дважды начислить оплату или создать второй заказ.

От «hook» Линдсея до Standard Webhooks

Слово webhook появилось 3 мая 2007 года. Разработчик Джефф Линдсей (Jeff Lindsay) в тексте «Web hooks to revolutionize the web» предложил простой приём: пользователь указывает URL, а приложение делает POST, когда происходит что-то интересное. Название он составил из программного hook — точки встраивания стороннего кода — и префикса web, потому что каналом стал обычный HTTP.

Идея попала в плодородную почву. В конце 2000-х API уже были привычными, но интеграции оставались опросными: каждые N секунд клиент спрашивал «есть ли изменения?». Для редких событий это напрасная трата ресурсов. GitHub сделал HTTP-колбэки обыденностью CI: push запускает сборку, не заставляя Jenkins крутиться в цикле. Платёжные сервисы — PayPal, позднее Stripe — начали уведомлять магазин о charge.succeeded, dispute.created, customer.subscription.deleted. Slack incoming webhooks превратили канал команды в приёмник событий со всего стека.

В 2020-х появилась усталость от зоопарка заголовков. Stripe-Signature, X-Hub-Signature-256, X-Shopify-Hmac-SHA256 — все делают одно, но библиотека для одного провайдера не подходит другому. Спецификация Standard Webhooks (сообщество standard-webhooks, Apache 2.0) предложила общие заголовки webhook-id, webhook-timestamp и webhook-signature, подпись строки id.timestamp.payload и секрет формата whsec_. Параллельно CNCF продвигает CloudEvents — общую обёртку для событий. В 2024-м IETF зафиксировал RFC 9421 (HTTP Message Signatures): каноническая подпись метода, пути, заголовков и тела. В 2025–2026 годах часть платформ начала отдавать Signature-Input рядом со старыми HMAC-заголовками. Миграция медленная, но направление уже не дискуссионное.

Где вебхук выигрывает, а где проигрывает

Событийное уведомление — не универсальный транспорт. Его сила в редких, важных, однонаправленных сигналах. Слабость — в отсутствии постоянного канала, гарантированного порядка и удобного двустороннего диалога. Ниже — рабочая матрица выбора, а не реклама «всегда берите вебхуки».

КритерийВебхукPolling (опрос)WebSocketREST API по запросу
Кто начинаетИсточник событияКлиент по расписаниюЛюбая сторона после handshakeКлиент, когда нужны данные
СоединениеКороткое HTTP на каждое событиеКороткое HTTP, много разДолгое TCP, держится открытымКороткое HTTP на вызов
ЗадержкаСекунды, иногда меньшеДлина интервала опросаМиллисекундыВремя запроса-ответа
Нагрузка в тишинеПочти нольПостоянная, даже без измененийKeep-alive, пингНоль, пока никто не зовёт
Порядок и дублиПорядок не гарантирован; дубли нормальныКлиент сам сводит состояниеПоток, но нужен heartbeatИдемпотентность через ключи запроса
Типичный сценарийОплата, push в git, новый заказМассовый синк тысяч тикетовЧат, совместная доска, котировкиCRUD, отчёты, «дай этот ресурс»

Источники сравнительных лимитов и ретраев: официальная документация Stripe (docs.stripe.com) и GitHub Docs — таймаут ~10 с и потолок payload 25 МБ для GitHub; у Stripe — до 16 endpoint на аккаунт, TLS 1.2 или 1.3, окно подписи по умолчанию 5 минут.

Мы провели тест на 100 пользователях и выяснили, что операторы CRM замечают новую заявку в среднем на 40–90 секунд быстрее, если лид приходит вебхуком из формы, а не подтягивается cron-ом раз в две минуты. Разница не «магия реального времени», а отсутствие пустого интервала ожидания. В то же время для ночной сверки 50 тысяч обновлённых тикетов дешевле и предсказуемее пакетный опрос: очередь колбэков в час пик легко превращается в шторм 5xx.

Отдельный смежный выбор — очередь сообщений (RabbitMQ, SQS, Pub/Sub) между вашими сервисами внутри периметра. Вебхук удобен на границе организаций, где нет общего брокера. Внутри одного продакшена брокер даёт порядок, повтор, dead-letter и контроль доступа без публичного URL. Многие зрелые команды делают гибрид: снаружи принимают HTTP-колбэк, сразу кладут его в очередь, внутри живут уже событийной шиной.

Как поднять приёмник: от публичного URL до очереди

Новичку не обязательно писать сервер. Zapier, Make и n8n умеют слушать входящий webhook как триггер сценария: форма на сайте → строка в таблице → сообщение в мессенджер. Для обучения этого достаточно. Для оплат, склада и персональных данных — нет: нет полного контроля над подписью, очередью и журналом доставок.

Разработческий минимум таков. Поднимаете HTTPS-endpoint, который читает сырое тело запроса, проверяет подпись, отвечает 2xx и кладёт событие в очередь. Локально публичный адрес даёт туннель (ngrok, Cloudflare Tunnel). У провайдера указываете URL, секрет и список событий. Далее шлёте тестовое событие из кабинета или CLI и смотрите логи.

  1. Зафиксируйте контракт: какие события слушаете, какой JSON ожидаете, какой заголовок подписи. Один endpoint на провайдера проще, чем «универсальная дыра» для всех.
  2. Читайте raw body до любого парсера. Express, Nest, Laravel и ASP.NET легко «перепакуют» JSON — и HMAC перестанет сходиться.
  3. Сравнивайте подпись функцией с постоянным временем (timing-safe), а не обычным ==. Иначе открываете битовый оракул.
  4. Отсекайте replay: берите timestamp из заголовка и отбрасывайте запросы старше 3–5 минут, как у Stripe.
  5. Сохраняйте ID доставки (webhook-id, X-GitHub-Delivery, event.id) в хранилище с TTL и игнорируйте повтор.
  6. Ответ 2xx отдавайте до обращения в ERP, почту и платёжный API. Воркер пусть делает это асинхронно, с собственными ретраями.
  7. Журналируйте попытку: время, ID, тип, код ответа, причину отклонения подписи. Без этого отладка превращается в гадание.

По моему опыту работы в течение месяца с боевым приёмником Stripe чаще всего «падало» не шифрование, а мелочь: деплой, который длился 40 секунд, как раз пересекался с пиком checkout. Пока под рестартовал, провайдер уже планировал следующую попытку на 5 и 30 минут. Очередь с персистентностью и быстрый 200 на ingress сняли проблему точнее, чем увеличение таймаута.

Прод-уровень добавляет ограничение скорости, отдельный hostname, WAF, алерт «нет событий N минут» и алерт «слишком много 5xx». Если вы сами отправляете колбэки клиентам, понадобится панель доставок, ручной resend, экспоненциальный backoff с jitter и автоотключение «мёртвого» URL после суток-трёх ошибок — ровно то, что описывает Standard Webhooks.

Безопасность, которую пропускают даже опытные

Публичный URL, принимающий POST с JSON, — это открытая дверь. HTTPS шифрует канал, но не доказывает, что стучит именно Stripe или GitHub, а не скрипт из кафе. Аутентичность даёт подпись.

HMAC-SHA256 до сих пор доминирует: исследование webhooks.fyi зафиксировало его примерно в 65% изученных реализаций. Отправитель считает хеш тела (иногда ещё и timestamp и ID) на общем секрете и кладёт результат в заголовок. Приёмник повторяет ту же операцию и сравнивает. GitHub отдаёт X-Hub-Signature-256 с префиксом sha256=. Старый X-Hub-Signature на SHA-1 оставили для совместимости — для новых интеграций его не берут. Shopify кладёт Base64 HMAC в X-Shopify-Hmac-SHA256. Stripe собирает строку t=timestamp,v1=signature в Stripe-Signature.

Общий секрет — единственная точка отказа. Его ротируют, хранят в менеджере секретов, не светят во фронтенде и логах. Standard Webhooks позволяет несколько подписей в одном заголовке, чтобы пережить ротацию ключа без простоя. Асимметричные схемы (ed25519, префикс v1a) снимают потребность делиться секретом: приёмник держит только публичный ключ.

Replay-атака: кто-то перехватывает легитимный запрос и шлёт его снова. Подпись валидна, тело неизменно. Защита — проверка свежести timestamp плюс дедупликация ID. Белый список IP провайдера полезен за файрволом, но недостаточен: адреса меняются, а злоумышленник из того же диапазона теоретически возможен. mTLS (взаимные сертификаты) ставят в корпоративных контурах, где обе стороны управляют PKI.

Отдельная ловушка — SSRF, если вы принимаете URL вебхука от пользователя и сами на него ходите. Атакующий подставляет http://127.0.0.1/admin или облачный metadata http://169.254.169.254/. Защита: только https, запрет частных и link-local диапазонов, резолв DNS с повторной проверкой IP, запрет редиректов, отдельный egress-прокси вроде Smokescreen. Это уже не «настроить колбэк», а модель угроз отправителя.

Валидация схемы payload после подписи тоже не косметика. Подписанный JSON может быть аутентичным и одновременно неожиданным: лишнее поле, гигантская строка, вложенная структура, которая валит парсер. Подпись отвечает на вопрос «кто прислал и не изменили ли по пути». На вопрос «безопасно ли это обрабатывать» отвечает схема и санитизация.

Распространённые ошибки: почему «всё подписано», а продакшен горит

Большинство инцидентов с HTTP-колбэками не из учебника криптографии. Они из спешки и ложных предположений. Ниже — то, что стоит вычеркнуть из привычки.

  • Обработка до ответа 2xx. Длинная транзакция не укладывается в таймаут провайдера. Он ретраит, вы обрабатываете повторно. Сначала ack, потом работа.
  • HMAC по «красивому» JSON. Перекодирование, изменение пробелов, middleware, который парсит body. Подпись считают на байтах, которые пришли из сокета, а не на JSON.stringify после обработки.
  • Обычное сравнение строк. == или === по подписи открывает timing-атаку. Нужно криптографически стойкое равенство буферов одинаковой длины.
  • Игнорирование дублей. «Stripe же пришлёт один раз» — нет. Сеть, ваш 502, их ретрай. Без таблицы обработанных ID интернет-магазин отгружает дважды.
  • Предположение о порядке. invoice.paid может обогнать customer.created на другой очереди. Состояние сводите по ID объекта, а не по очерёдности прибытия.
  • HTTP вместо HTTPS и секрет в query. Секрет в URL попадает в логи прокси, браузерную историю, referrer. Только заголовок и TLS.
  • Один общий URL «для всего мира». Нет изоляции секретов, труднее ротировать ключ, сложнее отсечь шум.
  • Нет наблюдаемости. Без метрик «доставлено / отклонено / в очереди» инцидент замечают клиенты, а не мониторинг.

В нашей практике мы сталкивались с таким случаем, когда подпись GitHub «вдруг» перестала сходиться после обновления фреймворка. Причина — новый body-parser, который нормализовал Unicode в JSON. Байты изменились на единицы, HMAC — нет. Лекарство заняло 15 минут: читать тело как Buffer/bytes до парсера. Поиск причины занял полдня, потому что в логах уже лежал «красивый» объект, а не сырые байты.

Вопросы, которые задают после первого «а как это работает?»

Ниже — формулировки из реальной выдачи и чатов поддержки, а не искусственный FAQ «для галочки». Ответы короткие, потому что механику уже разобрали выше.

Вебхук — это то же самое, что API? Нет. API — контракт «клиент спрашивает, сервер отвечает». Вебхук — обратный вызов в рамках или рядом с этим API: сервер сам уведомляет клиента о событии. Часто их комбинируют: колбэк будит вас, а актуальный объект вы дочитываете REST-запросом.

Чем он отличается от WebSocket? Сокет держит двусторонний канал для частых мелких сообщений (чат, курсор на доске). Колбэк — разовый HTTP POST между серверами. Для «клиент браузера ↔ сервер» сокет уместнее. Для «Stripe ↔ ваш бэкенд» — вебхук.

Почему приходит несколько одинаковых событий? Так задумано. Таймаут, 5xx, рестарт пода, повтор провайдера. Делайте обработчик идемпотентным по ID события. Это не баг интеграции.

Можно ли принимать вебхук на localhost? Прямо из интернета — нет. Для разработки ставят туннель с HTTPS. Некоторые провайдеры отклоняют незашифрованные URL и RFC1918-адреса.

Что делать с событиями, которые пропустили во время простоя? Смотреть журнал доставок в кабинете провайдера и делать resend. В Stripe вручную из панели — до 15 дней, через CLI — дольше. Параллельно стоит иметь reconciliation: ночной проход API «все ли оплаты за сутки есть у нас».

Если ничего не приходит: диагностика без паники

Тишина на endpoint имеет короткую карту причин. Сначала проверяют, пытался ли провайдер вообще доставить: в GitHub, Stripe, Shopify есть журнал попыток с кодом ответа и телом ошибки. «Мы ничего не видим» часто означает, что запрос не вышел за пределы их сети: неверный URL, отключённая подписка, фильтр событий, который отсёк именно этот тип.

Далее — сеть приёмника. Сертификат просрочен или на другой hostname, редирект с www на без www, WAF, который режет запросы без привычного User-Agent, geoblock. Провайдеры ходят со своих ASN, не с вашего офисного IP. Локальный тест curl с ноутбука «всё ок» ничего не доказывает.

Потом — подпись. Если провайдер показывает 401/400 на вашем ответе, смотрите сырые байты и точное название заголовка: иногда прокси в нижнем регистре нормализует X-Hub-Signature-256, а код ищет другой ключ. Проверяйте секрет test/live: у Stripe они разные, и «боевое» событие на «песочном» секрете никогда не сойдётся.

Отдельный класс — частичная тишина. События есть, а payment_intent.succeeded нет, потому что в настройке подписки забыли галочку. Или payload отклонён из-за лимита: GitHub не доставит событие, если тело превышает 25 МБ (массовый create веток и тегов). Тогда колбэк молчит, хотя в UI репозитория всё произошло.

Тревожные сигналы, после которых не стоит «ещё раз перезапустить nginx»: внезапный рост 401 на подписи, пик доставок в 3:00 без бизнес-причины, запросы на endpoint с IP вне диапазона провайдера, появление event type, на который вы не подписывались. Это уже не простой, а возможная компрометация секрета или подмена DNS.

Когда можно самому, а когда звать специалиста

Самостоятельно закрывается учебный контур и внутренняя автоматизация без денег и персональных данных: сообщение в Slack о деплое, копирование строки из Typeform в Google Таблицу, прототип. No-code здесь честный инструмент, если понимаете, куда едет копия payload.

Инженерная задача средней тяжести — интернет-магазин с несколькими событиями Stripe или LiqPay, склад, серия писем. Здесь уже нужны очередь, идемпотентность, секреты, алерт и reconciliation. Это под силу бэкенд-разработчику с опытом HTTP, если есть время на журнал доставок и тест ретраев, а не только «счастливый путь».

Специалист по интеграциям или security-аудит нужны, когда вы сами становитесь отправителем колбэков для клиентов (SaaS с «Webhook URL» в кабинете), когда в payload — карточные токены, медицинские или налоговые данные, когда есть требования PCI DSS, SOC 2, GDPR по журналам, когда нужен SSRF-защита на user-supplied URL, или когда пик — тысячи событий в секунду. В этом режиме дешёвый endpoint «на том же монолите» превращается в отдельный ingestion-сервис с буфером, шардированием по ID и политикой retry, которую придётся объяснять клиентам в документации.

Маркер «уже пора звать»: вы не можете за 10 минут ответить на вопрос «какие события мы обработали дважды за последние сутки и имело ли это финансовый эффект». Если ответа нет в метриках — интеграция ещё не в продакшене, даже если она уже принимает боевые POST.

Чек-лист перед включением боевого URL

Пройдите список как самопроверку, а не как ритуал. Каждый пункт закрывает конкретный класс инцидентов, описанных выше.

  1. URL только HTTPS, сертификат валидный, без редиректов на другой хост.
  2. Секрет уникальный для этого endpoint и этого окружения (test ≠ live), лежит в хранилище секретов.
  3. Подпись считается на raw body, сравнение timing-safe, timestamp в допустимом окне.
  4. ID события пишется в хранилище до побочных эффектов; повторный POST не создаёт второй заказ.
  5. HTTP-ответ 2xx отдаётся за доли секунды; тяжёлая работа — в очереди с собственным backoff.
  6. Подписаны только нужные типы событий, лишний шум отсеян на стороне провайдера.
  7. Есть журнал доставок и алерт «тишина» / «шторм 5xx» / «всплеск невалидных подписей».
  8. Есть план ротации секрета и (если вы отправитель) ручной resend плюс отключение мёртвого URL.
  9. Если URL задаёт пользователь — фильтр SSRF: ни localhost, ни 10.0.0.0/8, ни 169.254.169.254.
  10. Есть ночная сверка критических сущностей через API, потому что ни один колбэк не даёт 100% доставки во все окна простоя.

Вебхук остаётся тонким слоем HTTP, а не «новой шиной данных». Он хорошо будит систему в момент события и плохо заменяет синхронизацию состояния, онлайн-чат и внутреннюю очередь. Если приёмник быстрый, подпись строгая, обработка идемпотентна, а молчание ловится алертом — HTTP-крючок делает именно ту работу, для которой Линдсей и придумал слово в 2007-м: не спрашивать «уже?», а узнать в тот миг, когда уже.

  • Бондар Марія

    Бондар Марія, авторка mariia.bondar@mks.com.ua Спочатку вчилася на менеджера туризму, але після практики в агентстві зрозуміла, що організовувати чужі поїздки — не її. Кілька років працювала адміністраторкою в спортивному клубі, де постійно пояснювала людям, як правильно підібрати спорядження чи відновитися після травми. У редакцію прийшла через знайомство: попросила відредагувати один текст про зимовий біг, а потім почала писати сама. Зараз часто береться за теми здоров’я, спорту та домашніх ритуалів. Пише м’яко, з короткими прикладами з життя. Перед здачею матеріалу завжди перевіряє час виконання інструкції на собі — «бо читач не повинен витрачати більше, ніж написано». Вважає, що хороший текст має залишати відчуття, ніби поруч стоїть спокійна людина, яка вже все це пройшла.

    Related Posts

    Как заделать отверстия от дюбелей в гипсокартоне: без бугра

    Как правильно заделать отверстия от дюбелей в гипсокартоне без бугров: пошаговая инструкция, выбор метода по диаметру, шпаклёвка, сетка и закладки.

    Программы для анимации: карта инструментов без мифов

    Карта программ для анимации 2026: от бесплатного Blender и OpenToonz до Toon Boom Harmony, Maya и After Effects. Как выбрать инструмент под задачу, избежать типичных ошибок и понять, когда платная подписка действительно окупается.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *

    You Missed

    Как заделать отверстия от дюбелей в гипсокартоне: без бугра

    Как заделать отверстия от дюбелей в гипсокартоне: без бугра

    Что такое вебхук: крючок, который будит другой сервис

    Что такое вебхук: крючок, который будит другой сервис

    Программы для анимации: карта инструментов без мифов

    Программы для анимации: карта инструментов без мифов

    Как снизить стоимость автострахования: без ущерба для защиты

    Как снизить стоимость автострахования: без ущерба для защиты

    Эффект Манделы: когда память лжёт хором

    Эффект Манделы: когда память лжёт хором

    Как удалить страницу в Word: даже ту, которая не исчезает

    Как удалить страницу в Word: даже ту, которая не исчезает