Webhook (вебхук) это HTTP-запрос, который сервис сам отправляет на указанный вами URL, когда у него происходит событие: пришла оплата, в репозиторий запушили коммит, боту написали сообщение. Вместо того чтобы раз за разом запрашивать API «есть что-то новое?», приложение один раз сообщает сервису адрес, и данные о событии приходят почти в реальном времени.
- Что такое вебхук простыми словами
- Как работает вебхук
- Что такое webhook URL
- Чем вебхук отличается от API и опроса
- Пример: приём вебхука на локальном сервере Python
- Как отправить тестовый вебхук и проверить подпись
- Безопасность вебхуков: секрет и подпись
- Повторы, таймауты и дубли
- Примеры использования вебхуков
- Ограничения вебхуков
- Частые вопросы о вебхуках
- Чем вебхук отличается от API?
- Как проверить, что вебхук работает?
- Что делать, если вебхук не приходит?
- Вебхук и redirect URI в OAuth это одно и то же?
Что такое вебхук простыми словами
В обычной работе с API инициатор ваш код: клиент отправляет запрос, сервер отвечает. Вебхук переворачивает направление. Инициатором становится сервис-источник события, а ваше приложение работает сервером, который принимает входящие запросы.
Отправитель это система, где происходит событие (GitHub, платёжная система, Telegram), получатель это ваш обработчик. Данные о событии отправитель кладёт в тело запроса, чаще всего в формате JSON.
Пример из интернет-магазина. Покупатель оплатил заказ, банк подтвердил платёж, и платёжная система автоматически отправляет уведомление на URL магазина. Магазин меняет статус заказа на «оплачен» и ставит задачу на сборку. Без вебхука он узнал бы об оплате, только когда сам запросит API платёжной системы.
Как работает вебхук
- Вы создаёте вебхук в настройках сервиса или с помощью его API: указываете URL и выбираете события, о которых нужны уведомления.
- При наступлении события сервис формирует POST-запрос, тело которого содержит данные события.
- Приложение получателя принимает данные, проверяет, что запрос отправил нужный источник, и отвечает кодом 2xx, например 200 OK.
- Если ответа нет или код другой, доставка считается неудачной. Что дальше, зависит от сервиса: один повторит отправку сам, другой оставит это вам.
Например, при пуше в репозиторий GitHub отправляет заголовки X-GitHub-Event с именем события (push), X-GitHub-Delivery с уникальным id доставки и X-Hub-Signature-256 с подписью тела, если у вебхука задан секрет.
Что такое webhook URL
Это адрес, на который система будет отправлять уведомления о событиях, например https://example.com/webhook, где /webhook путь к обработчику. Он должен быть доступен из интернета: localhost на вашем ноутбуке сервис не увидит. Stripe принимает для регистрации только публичный адрес с HTTPS, Telegram отправляет обновления HTTPS-запросом и только на порты 443, 80, 88 или 8443.
Чем вебхук отличается от API и опроса
Вебхук не замена API, а другой механизм получения новых данных. API (application programming interface, интерфейс программирования приложений) отвечает, когда клиент его спросил. Без вебхука клиент сам опрашивает сервер (polling): отправляет запрос раз в N секунд и сравнивает ответ с прошлым. Например, опрос раз в 10 секунд даёт 86 400 / 10 = 8640 запросов в сутки на один ресурс, даже если событий не было ни одного. Если ресурсов сотни, количество запросов растёт, и можно быстро упереться в лимит API. Вебхук приходит, только когда событие произошло, почти в реальном времени.

| Опрос API (polling) | Вебхук | WebSocket | |
|---|---|---|---|
| Кто начинает обмен | клиент | сервис-отправитель | клиент открывает соединение |
| Соединение | новый запрос на каждый опрос | новый запрос на каждое событие | одно постоянное, данные идут в обе стороны |
| Нужен публичный адрес у получателя | нет | да | нет |
Оба подхода в одном сервисе есть у Telegram. Бот получает обновления либо опросом через метод getUpdates, либо вебхуком, заданным через setWebhook, и эти способы взаимоисключающие: пока вебхук установлен, getUpdates не работает.
После вебхука часто всё равно идут в API: уведомление сообщает о событии, а полную информацию или следующее действие приложение получает обычным запросом. Как устроены такие запросы, мы разбирали в статье про REST API.
Пример: приём вебхука на локальном сервере Python
Напишем приложение, которое принимает вебхуки в формате GitHub. Хватит стандартной библиотеки Python 3.14. Сервер читает сырое тело, проверяет подпись, отсеивает повторную доставку, сразу отвечает, а данные передаёт в очередь.

# receiver.py: приёмник вебхуков, Python 3.14, только стандартная библиотека
import hashlib, hmac, json, os, queue, threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SECRET = os.environ.get("WEBHOOK_SECRET", "dev-secret").encode()
seen, lock = set(), threading.Lock() # id принятых доставок
jobs = queue.Queue() # очередь событий на обработку
def valid(raw, header):
if not header:
return False
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), header.encode())
def worker():
while True:
event, data = jobs.get()
print(f"обработка: {event}, ветка {data.get('ref')}", flush=True)
class Hook(BaseHTTPRequestHandler):
def reply(self, code, text):
self.send_response(code)
self.send_header("Content-Length", str(len(text)))
self.end_headers()
self.wfile.write(text.encode())
print(f"ответ {code}: {text}", flush=True)
def do_POST(self):
raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
if not valid(raw, self.headers.get("X-Hub-Signature-256")):
return self.reply(401, "bad signature")
delivery = self.headers.get("X-GitHub-Delivery")
if not delivery:
return self.reply(400, "no delivery id")
try:
data = json.loads(raw)
except ValueError:
data = None
if not isinstance(data, dict):
return self.reply(400, "not a JSON object")
if self.headers.get("X-GitHub-Event") != "push":
return self.reply(202, "ignored") # другие события пример не обрабатывает
with lock: # id запоминаем только для принятой доставки
repeat = delivery in seen
seen.add(delivery)
if repeat:
return self.reply(200, "duplicate")
jobs.put((self.headers.get("X-GitHub-Event"), data)) # работу делает worker
self.reply(202, "accepted")
def log_message(self, *args):
pass # вместо стандартного журнала печатаем свой
threading.Thread(target=worker, daemon=True).start()
print("Жду вебхуки на http://127.0.0.1:8000/webhook", flush=True)
ThreadingHTTPServer(("127.0.0.1", 8000), Hook).serve_forever()
Подпись проверяется по байтам тела до json.loads. Если разобрать JSON и собрать обратно, байты изменятся: json.dumps(json.loads(b'{"ref":"refs/heads/dev"}')) вернёт {"ref": "refs/heads/dev"} с пробелом после двоеточия, и подпись не сойдётся. Работу выполняет отдельный поток worker, а обработчик кладёт событие в очередь и сразу отвечает 202: GitHub ждёт код 2xx не дольше 10 секунд, потом считает доставку неудачной. Доставка без X-GitHub-Delivery и тело, которое не JSON-объект (массив json.loads разберёт, но data.get на нём упадёт), получают 400, а id запоминается только после разбора. Всё, кроме push, получает 202 ignored без обработки.
Запустите сервер в первом окне терминала (в дистрибутивах, где команды python нет, запускайте python3):
python receiver.py
Жду вебхуки на http://127.0.0.1:8000/webhook
Как отправить тестовый вебхук и проверить подпись
Во втором окне отправим запросы через curl так, как их отправляет GitHub. Команды проверены в Git Bash на Windows, в Linux и macOS мы их не запускали. Подпись считает openssl dgst -hmac, флаг --json есть в curl с версии 7.82.0, -H @файл берёт заголовки из файла.
URL=http://127.0.0.1:8000/webhook
BODY='{"ref":"refs/heads/main"}'
SIG=sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "dev-secret" | sed 's/^.* //')
hook() { curl -s -w ' %{http_code}\n' -H "X-GitHub-Event: ${EVENT:-push}" "$@" $URL; }
hook -H "X-GitHub-Delivery: d1" -H "X-Hub-Signature-256: $SIG" --json "$BODY"
hook -H "X-GitHub-Delivery: d1" -H "X-Hub-Signature-256: $SIG" --json "$BODY"
EVENT=ping hook -H "X-GitHub-Delivery: d3" -H "X-Hub-Signature-256: $SIG" --json "$BODY"
hook -H "X-GitHub-Delivery: d2" -H "X-Hub-Signature-256: $SIG" --json '{"ref":"refs/heads/prod"}'
hook -H "X-Hub-Signature-256: $SIG" --json "$BODY"
hook -H "X-Hub-Signature-256: sha256=0000" --json "$BODY"
hook --json "$BODY"
printf 'X-Hub-Signature-256: sha256=\xc3\xa9\r\n' > bad-header.txt
hook -H @bad-header.txt --json "$BODY"
accepted 202
duplicate 200
ignored 202
bad signature 401
no delivery id 400
bad signature 401
bad signature 401
bad signature 401
На повтор с тем же X-GitHub-Delivery сервер отвечает 200, но второй раз событие в работу не идёт, третий запрос, событие ping, подтверждён без обработки. Четвёртый несёт настоящую подпись от другого тела, и сервер его отверг. Пятый подписан верно, но без id доставки, он получил 400. Дальше 401 получили чужая подпись, запрос без подписи и подпись с буквой é. В окне сервера для принятой доставки появляется строка из очереди: обработка: push, ветка refs/heads/main.
Последний запрос нужен для ловушки. Если сравнивать строки, hmac.compare_digest(expected, header), как в примере GitHub, заголовок с не-ASCII буквой роняет обработчик: в консоли TypeError: comparing strings with non-ASCII characters is not supported, у curl curl: (52) Empty reply from server. Строки она принимает только из ASCII, поэтому receiver.py сравнивает байты. Заголовок взят из файла: из аргумента -H Git Bash передал é как e.

Безопасность вебхуков: секрет и подпись
Адрес обработчика может узнать кто угодно и отправить на него выдуманное «оплачено». Проверка подлинности запросов закрывает эту дыру: GitHub и Stripe считают HMAC-SHA256 с общим секретом (GitHub по телу, Stripe по времени и телу) и кладут результат в заголовок, а вы считаете то же самое и сравниваете. Без секрета подпись не подделать, а любая правка тела её ломает. Чем HMAC отличается от обычного хеша, разобрано в статье про хеш-функции.
Свою функцию сверьте с примером GitHub, где даны секрет, тело и ожидаемый результат:
# github_vector.py: сверка с тестовым примером GitHub
import hashlib, hmac
sig = hmac.new(b"It's a Secret to Everybody", b"Hello, World!", hashlib.sha256)
print("sha256=" + sig.hexdigest())
print(sig.hexdigest() == "757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17")
sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
True
У каждого сервиса своя схема, и правила GitHub на другие системы не переносятся:
| Сервис | Заголовок | Что в нём |
|---|---|---|
| GitHub | X-Hub-Signature-256 | sha256= и HMAC-SHA256 тела в hex |
| Stripe | Stripe-Signature | время t= и подпись v1= от строки «время, точка, тело»; библиотеки Stripe по умолчанию отвергают запрос, если метка времени в заголовке расходится с текущим временем больше чем на 5 минут |
| Telegram | X-Telegram-Bot-Api-Secret-Token | подписи нет, приходит сам секрет из параметра secret_token метода setWebhook |
Подпись и секрет сравнивайте через hmac.compare_digest(): обычный == прерывает сравнение на первом несовпадении, и по времени ответа атакующий может подбирать подпись (атака по времени). compare_digest сравнивает без такого раннего выхода. Принимайте вебхуки по HTTPS, а секрет делайте длинной случайной строкой; мы берём, например, secrets.token_hex(32).
Повторы, таймауты и дубли
| Сервис | Сколько ждёт ответа | Повторяет ли неудачную доставку |
|---|---|---|
| GitHub | 2xx в течение 10 секунд | нет, повтор вручную, с тем же X-GitHub-Delivery |
| Stripe | 2xx нужно вернуть быстро, до сложной логики | в боевом режиме до трёх дней с растущим интервалом |
| Telegram | в описании Bot API срок не указан | при коде не 2xx повторяет и после «разумного числа попыток» прекращает |
Отвечайте сразу, а тяжёлую обработку выполняйте асинхронно, через очередь. Будьте готовы к дублям: у Stripe одно событие может прийти больше одного раза, у GitLab дубль приходит, если вебхук не дождался ответа. Запоминайте id обработанных событий и второй раз их не обрабатывайте, чтобы заказ не ушёл в сборку дважды. Не рассчитывайте на порядок: Stripe не гарантирует, что события придут в том порядке, в каком возникли.

Примеры использования вебхуков
- Разработка и CI/CD: пуш в главную ветку GitLab запускает скрипт деплоя, чат команды получает оповещения о завершении пайплайна, внешние трекеры задач обновляются, системы мониторинга следят за активностью в репозиториях.
- Платежи: платёжная система отправляет интернет-магазину уведомления об успешной оплате, например событие
payment_intent.succeededв Stripe. - Чат-боты: Telegram отправляет каждое новое обновление боту на webhook URL в виде JSON.
- Автоматизация без кода: в n8n узел Webhook принимает данные от приложений и сервисов, когда у них происходит событие.
Ограничения вебхуков
Получателю нужен публичный адрес. Для отладки на своём компьютере разработчики используют инструменты, которые пересылают запросы на localhost: для GitHub это smee.io, для Stripe ngrok или Stripe CLI.

Если обработчик недоступен, данные можно потерять: GitHub неудачную доставку сам не повторяет, её доставляют вручную после восстановления сервера, Telegram сдаётся после «разумного числа попыток» и хранит обновления не дольше 24 часов. Очередь в нашем примере живёт в памяти и пропадает при перезапуске, в проекте её заменяют брокером сообщений или таблицей в базе данных.
Объём данных ограничен: у GitHub 25 МБ на доставку. Stripe считает редирект неудачной доставкой, поэтому указывайте конечный адрес. Модуль http.server не рекомендуется для продакшена и выполняет только базовые проверки безопасности. Рабочий приёмник пишут на фреймворке вроде Flask, FastAPI или Django, логика проверки подписи остаётся той же.
Опрос API лучше использовать, когда у приложения нет публичного адреса или сервис не предоставляет вебхуки. В остальных случаях выигрывает вебхук: опрос раз в 10 секунд тратит 8640 запросов в сутки, даже если событий не было.
Частые вопросы о вебхуках
Чем вебхук отличается от API?
Разница в том, кто начинает обмен: API отвечает на запрос клиента, вебхук приходит сам, когда произошло событие. Их часто используют вместе: за подробностями после вебхука идут в API.
Как проверить, что вебхук работает?
Поднимите локальный приёмник, как в примере выше, и проверьте через curl правильной и поддельной подписью. Затем пробросьте его наружу и отправьте настоящее событие из сервиса.
Что делать, если вебхук не приходит?
Откройте журнал доставок у отправителя: у Stripe это вкладка Event deliveries с кодом ответа по каждой попытке, у Telegram метод getWebhookInfo с последней ошибкой доставки. Затем проверьте, что адрес открывается из интернета, обработчик отвечает 2xx и успевает ответить, а сервер не отдаёт редирект.
Вебхук и redirect URI в OAuth это одно и то же?
Нет. В OAuth 2.0 сервер авторизации перенаправляет браузер пользователя на redirect URI, а вебхук сервис отправляет прямо на ваш сервер, браузер в этом не участвует.








