Что такое webhook и как сервисы сообщают друг другу о событиях
Представим интернет-магазин, который ожидает подтверждение оплаты.
После создания платежа магазин может регулярно обращаться к платёжному сервису:
GET /payments/pay_5831
И каждый раз получать состояние:
{
"id": "pay_5831",
"status": "pending"
}
Через несколько секунд запрос повторяется. Затем ещё раз. Большую часть времени статус не меняется, но магазин всё равно продолжает проверку.
Такой способ называется polling, или периодический опрос.
Приложение
↓
запрашивает состояние
↓
получает ответ
↓
ждёт
↓
запрашивает снова
Polling работает, но создаёт несколько проблем:
- запросы отправляются даже тогда, когда ничего не произошло;
- приложение узнаёт об изменении только во время следующей проверки;
- приходится выбирать частоту опроса;
- при большом количестве объектов растёт нагрузка;
- сложнее быстро реагировать на редкие события.
Можно изменить направление взаимодействия:
Не магазин постоянно спрашивает о платеже
-
платёжный сервис сам сообщает,
когда платёж завершён
Для этого используются вебхуки.
Что такое webhook
Webhook - HTTP-запрос, который один сервис автоматически отправляет другому при наступлении определённого события.
Например:
Покупатель завершил оплату
-
платёжный сервис создаёт событие payment.succeeded
-
отправляет POST-запрос магазину
-
магазин получает данные
-
меняет статус заказа
Webhook не является отдельным сетевым протоколом. Обычно это привычный HTTP-запрос, чаще всего POST, отправленный на заранее зарегистрированный адрес.
Такой адрес называется webhook URL или webhook endpoint.
Пример:
https://shop.example.com/webhooks/payment
Endpoint - конкретная точка входа в приложение, которая принимает запросы определённого назначения.
HTTP-методы GET, POST, PUT, PATCH, DELETE и QUERY: примеры запросов
Какие компоненты участвуют в webhook
Чтобы не путаться в направлении запроса, полезно разделить роли.
Источник события
Источник - сервис, в котором что-то произошло.
Например:
- платёжная система зафиксировала оплату;
- CRM создала новую сделку;
- Telegram получил сообщение для бота;
- GitHub получил новый commit;
- сервис доставки изменил статус заказа.
Источник события также является отправителем webhook-запроса.
Получатель
Получатель - приложение, которое должно отреагировать.
Им может быть:
- сервер интернет-магазина;
- CRM;
- n8n;
- Telegram-бот;
- Python-приложение;
- системные аналитики;
- собственный backend.
Событие
Событие - факт изменения, важный для другой системы.
Примеры названий:
payment.succeeded
payment.failed
order.created
lead.updated
message.received
delivery.completed
Названия событий определяет конкретный сервис. Универсального стандарта для них нет.
Webhook endpoint
Endpoint - публичный URL, на который отправитель отправляет событие:
POST https://shop.example.com/webhooks/payment
Общая схема:
Событие произошло у отправителя
↓
отправитель формирует HTTP-запрос
↓
запрос приходит на webhook endpoint
↓
получатель проверяет и обрабатывает событие
Как webhook выглядит изнутри
Рассмотрим пример webhook от платёжного сервиса.
POST /webhooks/payment HTTP/1.1
Host: shop.example.com
Content-Type: application/json
X-Webhook-Id: evt_84721
X-Webhook-Signature: sha256=9f8d...
{
"event": "payment.succeeded",
"payment_id": "pay_5831",
"order_id": "order_1045",
"amount": 4900,
"currency": "RUB",
"created_at": "2026-08-01T12:45:00Z"
}
Разберём запрос по частям.
HTTP-метод
POST
POST используется, потому что отправитель передаёт получателю новое событие и связанный с ним набор данных.
Некоторые сервисы могут использовать другие методы, но POST является самым распространённым вариантом.
URL
/webhooks/payment
Это маршрут внутри приложения-получателя.
Полный адрес:
https://shop.example.com/webhooks/payment
Заголовки
Content-Type: application/json
X-Webhook-Id: evt_84721
X-Webhook-Signature: sha256=9f8d...
Заголовки передают служебную информацию:
- формат тела;
- идентификатор события;
- подпись;
- версию схемы;
- время отправки;
- идентификатор доставки.
Набор заголовков зависит от конкретного провайдера.
Тело запроса
{
"event": "payment.succeeded",
"payment_id": "pay_5831",
"order_id": "order_1045",
"amount": 4900,
"currency": "RUB"
}
Тело содержит данные, необходимые для обработки события.
Важно понимать:
Формат webhook не универсален. Перед интеграцией нужно прочитать документацию конкретного сервиса: какие события доступны, какие поля приходят, какие заголовки используются и какой ответ ожидается.
Что происходит после получения webhook
Получить JSON недостаточно. Надёжный обработчик выполняет несколько последовательных действий.
Типовая последовательность:
1. Принять HTTP-запрос.
2. Проверить формат.
3. Проверить подлинность отправителя.
4. Определить тип события.
5. Проверить обязательные поля.
6. Проверить, не обрабатывалось ли событие раньше.
7. Сохранить событие или передать его в очередь.
8. Вернуть успешный HTTP-ответ.
9. Выполнить бизнес-действие.
Порядок отдельных шагов может меняться, но три задачи остаются обязательными:
- не доверять запросу без проверки;
- не выполнять одно действие дважды;
- не заставлять отправителя слишком долго ждать ответа.
Почему нужно быстро отвечать
Представим обработчик, который после получения webhook выполняет всё синхронно:
получить событие
↓
обратиться к CRM
↓
создать документ
↓
отправить письмо
↓
отправить сообщение в Telegram
↓
обновить аналитику
↓
вернуть 200 OK
Если один из внешних сервисов отвечает медленно, webhook endpoint может не успеть вернуть ответ.
Отправитель увидит timeout и решит, что доставка не удалась.
Более надёжная схема:
получить webhook
↓
проверить запрос
↓
сохранить событие
↓
быстро вернуть 2xx
↓
обработать событие отдельно
Для фоновой обработки применяют:
- очередь сообщений;
- фоновые задачи;
- worker-процессы;
- отдельный workflow;
- внутреннюю таблицу необработанных событий.
Для небольшой автоматизации допустима синхронная обработка, если она выполняется быстро и надёжно. Но по мере роста системы отделение приёма события от бизнес-логики становится важнее.
Какой ответ ожидает отправитель
После приёма webhook получатель возвращает HTTP-ответ.
Обычно успешный ответ выглядит так:
HTTP/1.1 200 OK
Или:
HTTP/1.1 204 No Content
Коды группы 2xx обычно означают:
Запрос принят успешно
Однако точные требования определяет отправитель. Некоторые сервисы ожидают конкретный код, тело или заголовок.
Что означает успешный ответ
200 OK не обязательно означает, что весь бизнес-процесс уже завершён.
Он может означать только:
Событие принято
↓
проверено
↓
сохранено для дальнейшей обработки
Это важное различие:
Техническое подтверждение
↓
webhook доставлен
↓
Бизнес-результат
↓
заказ обработан, письмо отправлено, CRM обновлена
Техническое подтверждение обычно нужно вернуть раньше.
Что происходит при ошибке
Если получатель вернул:
500 Internal Server Error
не ответил вовремя или оказался недоступен, отправитель может повторить доставку.
Правила повторов различаются:
- повтор через несколько секунд;
- постепенное увеличение интервала;
- ограниченное число попыток;
- повторы в течение нескольких часов или дней;
- ручная повторная отправка из панели сервиса.
Почему одно событие может прийти несколько раз
Повторная доставка не всегда означает ошибку отправителя.
Рассмотрим последовательность:
1. Получатель принял событие.
2. Обновил заказ.
3. Сформировал ответ 200 OK.
4. Ответ потерялся в сети.
5. Отправитель не получил подтверждение.
6. Событие отправлено повторно.
С точки зрения отправителя доставка не подтверждена. С точки зрения получателя действие уже выполнено.
Поэтому многие webhook-системы работают по принципу:
Событие может быть доставлено как минимум один раз.
Это означает, что получатель должен уметь безопасно принимать дубликаты.
Идемпотентность
Представим webhook об успешной оплате:
{
"event_id": "evt_84721",
"event": "payment.succeeded",
"order_id": "order_1045"
}
Если обработчик каждый раз создаёт отгрузку, повторная доставка приведёт к двум отгрузкам.
Нужна проверка:
Событие evt_84721 уже обработано?
↓
да - не повторять действие
↓
нет - выполнить и сохранить идентификатор
Идемпотентная обработка - обработка, при которой повторная доставка одного и того же события не создаёт повторного бизнес-эффекта.
Как добиться идемпотентности
Базовый способ:
- Получить уникальный
event_id. - Проверить его в хранилище.
- Если ID уже существует, вернуть успешный ответ без повторного действия.
- Если ID новый, выполнить обработку.
- Сохранить ID как обработанный.
Псевдокод:
if event_id_already_processed(event_id):
return {"received": True}
process_event(payload)
save_processed_event(event_id)
return {"received": True}
Но в реальной системе между проверкой и сохранением возможна гонка:
два одинаковых запроса пришли одновременно
↓
оба не нашли event_id
↓
оба выполнили действие
Поэтому для надёжной реализации используют:
- уникальное ограничение в базе данных;
- транзакцию;
- атомарную вставку;
- блокировку;
- идемпотентный ключ в целевой операции.
Пример с SQL-логикой:
INSERT event_id
↓
если вставка успешна, обрабатывать
↓
если нарушено уникальное ограничение, событие уже было
Порядок доставки событий
Webhook-события не всегда приходят строго в том порядке, в котором произошли.
Например:
order.updated
order.cancelled
Из-за сетевых задержек получатель может сначала получить order.cancelled, а затем более старое order.updated.
Если обработчик слепо применит оба события, отменённый заказ снова станет активным.
Для защиты используют:
- время создания события;
- номер версии;
- последовательный номер;
- запрос актуального состояния через API;
- проверку допустимости перехода статуса.
Пример:
{
"event": "order.updated",
"order_id": "order_1045",
"version": 7
}
Если в базе уже сохранена версия 8, событие версии 7 устарело и не должно менять состояние.
Webhook и API: это не противоположности
Webhook обычно работает поверх HTTP и часто является частью API-интеграции.
Различие находится в том, кто начинает обмен.
Обычный API-запрос
Ваше приложение само обращается к сервису:
Ваше приложение
↓
GET /payments/pay_5831
↓
платёжный сервис
Webhook
Внешний сервис обращается к вашему приложению:
Платёжный сервис
↓
POST /webhooks/payment
↓
ваше приложение
Сравнение:
| Вопрос | Обычный API-запрос | Webhook |
|---|---|---|
| Кто начинает обмен | Ваше приложение | Внешний сервис |
| Когда происходит | По инициативе клиента | При наступлении события |
| Основная задача | Получить или изменить данные | Уведомить об изменении |
| Нужен публичный endpoint | Не всегда | Обычно да |
| Возможны повторы | Зависит от операции | Нужно считать нормальным сценарием |
На практике webhook и API используются вместе.
Например:
Webhook сообщает:
payment.succeeded
API-запрос получает:
актуальные данные платежа
Это особенно полезно, если webhook содержит только идентификатор события или если критически важно сверить состояние с источником.
Webhook и polling
Webhook не всегда полностью заменяет polling.
Webhook подходит, когда
- сервис поддерживает нужное событие;
- реакция должна происходить быстро;
- события возникают нерегулярно;
- получатель может принимать входящие запросы;
- нужно снизить число бессмысленных проверок.
Polling подходит, когда
- сервис не поддерживает вебхуки;
- приложение не может принимать входящие соединения;
- состояние нужно сверять по расписанию;
- важна регулярная полная синхронизация;
- требуется восстановление после пропущенных событий.
Надёжные интеграции часто используют оба механизма:
Webhook - быстро сообщает об изменении
Периодическая сверка через API - находит пропущенные события и расхождения
Например, webhook мгновенно обновляет статус оплаты, а ночная задача сверяет все платежи за сутки.
Как проверить подлинность webhook
Webhook endpoint обычно доступен из интернета. Если злоумышленник узнает URL, он может попытаться отправить поддельный запрос.
Нельзя считать запрос настоящим только потому, что JSON выглядит правильно.
HTTPS
Webhook URL должен использовать HTTPS:
https://example.com/webhooks/payment
HTTPS защищает данные при передаче и подтверждает подлинность сервера-получателя.
Но HTTPS сам по себе не доказывает, что запрос отправил нужный сервис.
Секрет в заголовке
Простейший вариант:
X-Webhook-Token: secret-value
Получатель сравнивает токен с ожидаемым.
Это лучше, чем отсутствие проверки, но секрет передаётся в явном виде внутри защищённого соединения и не защищает тело от всех сценариев повторного воспроизведения.
Криптографическая подпись
Более надёжный подход - HMAC-подпись.
Отправитель:
исходное тело запроса
+
секрет
-
HMAC-подпись
Подпись передаётся в заголовке:
X-Webhook-Signature: sha256=9f8d...
Получатель:
- Берёт исходные байты тела.
- Использует тот же секрет.
- Вычисляет собственную подпись.
- Сравнивает значения безопасным способом.
Если подписи совпадают, можно сделать два вывода:
- запрос, вероятно, создал владелец секрета;
- тело не было изменено после подписания.
Почему нужны исходные байты
Подпись обычно вычисляется по исходному телу запроса.
Если сначала разобрать JSON, а затем снова сериализовать его, могут измениться:
- пробелы;
- порядок полей;
- формат чисел;
- экранирование символов.
Логически JSON останется тем же, но набор байтов изменится, и подпись перестанет совпадать.
Поэтому многие фреймворки требуют получить raw body до JSON-парсинга.
Timestamp и защита от replay-атак
Злоумышленник может перехватить корректно подписанный запрос и повторить его позже.
Для защиты отправитель добавляет время:
X-Webhook-Timestamp: 1785597900
Получатель проверяет:
- подпись;
- допустимый возраст запроса;
- уникальность
event_id.
Например, запросы старше пяти минут можно отклонять.
Обработка webhook в FastAPI
Рассмотрим полный, но всё ещё упрощённый пример.
from typing import Any
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
processed_events: set[str] = set()
@app.post("/webhooks/orders")
async def receive_order_webhook(
request: Request,
) -> dict[str, Any]:
payload = await request.json()
event_id = payload.get("event_id")
event_type = payload.get("event")
if not event_id or not event_type:
raise HTTPException(
status_code=400,
detail="event_id and event are required",
)
if event_id in processed_events:
return {
"received": True,
"duplicate": True,
}
if event_type == "order.created":
order = payload.get("data", {})
print("New order:", order)
elif event_type == "order.cancelled":
order = payload.get("data", {})
print("Cancelled order:", order)
else:
return {
"received": True,
"ignored": True,
}
processed_events.add(event_id)
return {
"received": True,
"duplicate": False,
}
Этот код показывает основную структуру:
получить JSON
↓
проверить обязательные поля
↓
проверить event_id
↓
выбрать обработчик по типу события
↓
вернуть ответ
Но хранить processed_events в памяти нельзя в production:
- данные исчезнут после перезапуска;
- несколько процессов не разделяют один
set; - параллельные запросы создают гонки;
- память не предназначена для долгосрочного хранения.
Нужна база данных или другое общее хранилище.
Webhook в n8n
В n8n входной точкой обычно служит узел Webhook.
Он:
- создаёт URL;
- принимает HTTP-запрос;
- передаёт данные следующим узлам;
- может возвращать ответ автоматически или через отдельный узел ответа.
Типовой workflow:
Webhook
↓
проверка подписи
↓
проверка event_id
↓
Switch по типу события
↓
обновление CRM
↓
уведомление в Telegram
↓
сохранение результата
Respond to Webhook
Ответ можно вернуть:
- сразу после получения;
- после выполнения workflow;
- через отдельный узел
Respond to Webhook.
Для долгих процессов лучше быстро подтвердить приём, а тяжёлую логику выполнять отдельно.
Доступность n8n
Если n8n работает локально, внешний сервис не сможет вызвать:
http://localhost:5678/webhook/...
Нужен публично доступный HTTPS-адрес.
Для разработки применяют туннели, а для постоянной работы:
- домен;
- reverse proxy;
- TLS-сертификат;
- сервер с публичным адресом.
Версионирование webhook
Со временем формат события меняется.
Например, первая версия:
{
"event": "order.created",
"order_id": "order_1045"
}
Новая версия:
{
"type": "order.created",
"data": {
"order": {
"id": "order_1045"
}
}
}
Если изменить формат без предупреждения, старые обработчики перестанут работать.
Для совместимости используют:
- версию в URL;
- версию в заголовке;
- версию события;
- период поддержки старой схемы.
Пример:
/webhooks/v1/orders
/webhooks/v2/orders
Или:
X-Webhook-Version: 2026-08-01
Получатель не должен предполагать, что неизвестные поля являются ошибкой. Добавление нового необязательного поля обычно не должно ломать обработку.
Что логировать
Без логов webhook-интеграцию трудно отлаживать.
Полезно сохранять:
- время получения;
event_id;- тип события;
- источник;
- HTTP-статус;
- длительность обработки;
- результат проверки подписи;
- номер попытки;
- причину ошибки;
- связь с бизнес-объектом.
Но нельзя бездумно сохранять весь payload. В нём могут находиться:
- персональные данные;
- платёжная информация;
- токены;
- адреса;
- медицинские или иные чувствительные сведения.
Логи должны быть достаточны для диагностики, но не превращаться в незащищённую копию всех данных.
Повторные попытки и exponential backoff
Если обработчик временно недоступен, отправитель повторяет запрос.
Простой вариант:
через 10 секунд
через 10 секунд
через 10 секунд
Более устойчивый вариант - увеличивать задержку:
10 секунд
30 секунд
2 минуты
10 минут
1 час
Это называется exponential backoff - постепенное увеличение интервала между попытками.
Он снижает нагрузку на уже нестабильную систему.
Иногда добавляют случайный разброс задержки, чтобы множество повторов не пришло одновременно.
После исчерпания попыток событие может попасть в:
- журнал неудачных доставок;
- dead-letter queue;
- панель ручного повтора;
- отдельную таблицу ошибок.
Ошибки, которые стоит повторять, и ошибки, которые не стоит повторять
Не всякая ошибка требует повторной доставки.
Временные ошибки
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
timeout
Повтор позже может помочь.
Постоянные ошибки
400 Bad Request
401 Unauthorized
404 Not Found
Если URL или подпись неправильные, многократный повтор того же запроса обычно ничего не исправит.
Но окончательные правила задаёт конкретный сервис. Некоторые отправители повторяют все ответы, кроме 2xx.
Webhook как событие, а не команда
Полезно различать два вида сообщений.
Событие
payment.succeeded
order.created
delivery.completed
Событие сообщает о факте, который уже произошёл.
Команда
create_order
send_email
cancel_payment
Команда просит другую систему выполнить действие.
Webhook чаще используют для событий:
Произошло X
Это снижает связанность систем. Отправителю не обязательно знать, какие действия выполнит получатель.
Например, на payment.succeeded разные системы могут:
- обновить заказ;
- отправить чек;
- начислить бонусы;
- записать событие в аналитику.
Где используются вебхуки
Платежи
payment.succeeded
↓
обновить статус заказа
↓
запустить сборку
↓
отправить чек
CRM
lead.created
↓
назначить менеджера
↓
создать задачу
↓
отправить уведомление
Telegram-боты
пользователь отправил сообщение
↓
Telegram вызывает webhook
↓
бот обрабатывает update
↓
отправляет ответ через API
GitHub и CI/CD
push в репозиторий
↓
GitHub отправляет событие
↓
запускаются тесты
↓
после проверки выполняется развёртывание
Доставка
статус изменился
↓
обновить заказ
↓
уведомить клиента
Формы сайта
пользователь отправил форму
↓
webhook передал данные
↓
создана сделка в CRM
↓
менеджер получил сообщение
Типичные ошибки при работе с вебхуками
Считать, что событие придёт только один раз
Повторы являются нормальной частью надёжной доставки.
Выполнять долгую работу до HTTP-ответа
Это увеличивает вероятность timeout и повторной отправки.
Доверять любому запросу
Нужно проверять подпись, токен или другой механизм аутентификации.
Проверять подпись после изменения тела
Подпись часто вычисляется по исходным байтам.
Игнорировать порядок событий
Поздно пришедшее старое событие может перезаписать новое состояние.
Возвращать 500 для неизвестного события
Если событие корректное, но обработчик его не использует, иногда лучше подтвердить приём и проигнорировать. Иначе отправитель будет повторять его бесконечно.
Считать webhook единственным источником истины
Для критических операций полезно подтверждать актуальное состояние через API.
Не хранить историю доставки
Без event_id, статусов и логов трудно разбирать дубликаты и пропуски.
Как отлаживать webhook
Полезная последовательность:
1. Проверить, что endpoint публично доступен.
2. Убедиться, что HTTPS настроен.
3. Отправить тестовый запрос вручную.
4. Проверить HTTP-статус.
5. Посмотреть заголовки и raw body.
6. Проверить JSON.
7. Проверить подпись.
8. Повторно отправить то же event_id.
9. Смоделировать timeout и 500.
10. Проверить порядок событий.
Пример тестового запроса через curl:
curl -X POST \
https://example.com/webhooks/orders \
-H "Content-Type: application/json" \
-d '{
"event_id": "evt_test_001",
"event": "order.created",
"data": {
"order_id": "order_1045"
}
}'
После первого запроса полезно отправить его ещё раз и проверить, что бизнес-действие не повторилось.
Когда webhook не нужен
Webhook не является обязательным решением для любой интеграции.
Он может быть избыточен, если:
- данные нужны только по запросу пользователя;
- состояние меняется редко и проверяется раз в сутки;
- сервис не поддерживает события;
- нет публичного endpoint;
- обычный периодический импорт проще и надёжнее;
- процесс полностью локальный;
- нет необходимости реагировать быстро.
Пример:
Раз в ночь загрузить остатки товаров
-
polling или scheduled API request
Мгновенно узнать об успешной оплате
-
webhook
Выбор зависит от характера процесса, а не от популярности технологии.
Итог
Webhook - это событийный HTTP-запрос, который один сервис отправляет другому после изменения состояния.
Основной путь:
Событие
↓
HTTP POST
↓
webhook endpoint
↓
проверка
↓
подтверждение
↓
обработка
Для простого демо достаточно принять JSON и вернуть 200 OK.
Для надёжной интеграции необходимо учитывать:
- проверку подписи;
- исходное тело запроса;
- повторную доставку;
- идемпотентность;
- порядок событий;
- быстрый ответ;
- retries;
- логирование;
- хранение состояния;
- периодическую сверку через API.
Webhook не заменяет API. Он сообщает, что событие произошло, а API позволяет запросить данные или выполнить действие.
Именно сочетание webhook, API и надёжной обработки событий позволяет строить автоматизации, которые реагируют быстро и не создают дубликаты при сетевых сбоях.