Заметки

Webhook: как работают вебхуки и чем отличаются от API

M
Markabus
·
Конверт с карточкой события, звонок и лоток принятых уведомлений; повторный конверт отложен отдельно. Иллюстрация, сгенерировано ИИ.

Webhook, или вебхук, — способ сообщить другому приложению о событии: сервис сам отправляет HTTP-запрос на заранее указанный адрес. Например, документ готов — система уведомляет ваш сайт, а тот ставит задачу на обработку. Не приходится каждые несколько секунд спрашивать, появился ли результат.

Сам запрос обычно прост. Основная работа начинается вокруг него: проверить отправителя, принять повторную доставку без повторного действия и сохранить событие так, чтобы оно пережило перезапуск сервера. Разберём этот путь на примере интеграции, которой нужно получить готовый документ и запустить следующий шаг.

Как работает вебхук

Вы создаёте в своём приложении адрес приёма, например POST /webhooks/documents, и регистрируете его у сервиса. Там же выбираете нужные события и настраиваете секрет для проверки подписи, если такой механизм предусмотрен. После события сервис обращается к вашему обработчику.

В учебной интеграции тело запроса может выглядеть так. Это наш условный формат, а не схема конкретного провайдера:

{
  "id": "evt_demo_42",
  "type": "document.ready",
  "data": {
    "document_id": "doc_17"
  }
}

Поле id обозначает событие, а document_id — объект, к которому оно относится. Это разные сущности. У одного документа могут быть события создания, готовности и удаления. Вместо полного содержимого отправитель иногда передаёт только идентификатор: подробности получатель запрашивает через API.

Webhook и API: в чём разница

Вебхук не заменяет API и часто является частью интеграции с ним. При обычном запросе ваше приложение инициирует обращение: «покажи состояние документа». При вебхуке инициатором доставки выступает внешний сервис: «состояние изменилось». HTTP используется в обоих случаях.

СценарийКто начинает обменКогда подходит
Запрос к APIВаше приложениеНужно получить или изменить конкретный объект
Периодический опрос APIВаш планировщикВебхуков нет или нужна контрольная сверка
WebhookИсточник событияНужно реагировать на изменение без постоянного опроса

Если результат появляется через минуту или через час, опрос с коротким интервалом создаёт много пустых обращений. Вебхук помогает сократить их число. Но обещать мгновенную и гарантированную доставку нельзя: отправитель, сеть и ваш сервер могут быть недоступны. Для важных процессов полезна дополнительная сверка состояния через API.

Основы запросов разобраны в статье про REST API WordPress. В готовых сценариях адрес приёма часто создаёт платформа автоматизации — например, n8n. При этом проверка подлинности и защита от дублей всё равно остаются частью решения.

Как убедиться, что событие прислал нужный сервис

Случайный адрес обработчика не является полноценной защитой: URL может оказаться в журнале или настройках интеграции. Для публичного приёма используйте HTTPS и предусмотренный провайдером механизм проверки. Если сервис подписывает запросы, проверяйте подпись до выполнения действий с данными.

Один из вариантов — HMAC: отправитель и получатель знают общий секрет, а подпись зависит от переданных байтов. У GitHub для этого используется заголовок X-Hub-Signature-256. В официальной инструкции описан HMAC-SHA256. Алгоритм, имена заголовков и состав подписанных данных у других сервисов могут отличаться.

Для Node.js короткая функция проверки такого формата выглядит следующим образом. На входе rawBody — исходный Buffer тела запроса, сохранённый до JSON-разбора:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  if (!secret) throw new Error('Webhook secret is missing');
  if (typeof header !== 'string' ||
      !/^sha256=[a-f0-9]{64}$/i.test(header)) return false;
  const expected = createHmac('sha256', secret)
    .update(rawBody).digest();
  const received = Buffer.from(header.slice(7), 'hex');
  return timingSafeEqual(expected, received);
}

Проверка формата гарантирует одинаковую длину буферов для сравнения. Секрет должен поступать из защищённой конфигурации, а не из тела запроса. Функция показывает только проверку подписи: она не создаёт HTTP-сервер, не ограничивает размер запроса и не реализует очередь. Для готовой интеграции предпочтителен официальный SDK провайдера.

Мы проверили эту функцию локально: исходное тело прошло проверку; изменённое содержимое, неверная подпись, отсутствующий и повреждённый заголовки были отклонены. Тот же JSON после добавления отступов тоже не прошёл — байты изменились. Отсутствующий секрет вызвал ошибку конфигурации. Эти проверки относятся к функции, а не к доставке из внешнего сервиса.

Почему нельзя сначала пересобрать JSON

Объекты {"a":1} и { "a": 1 } для приложения могут означать одно и то же, но последовательности байтов различаются. Если разобрать тело и затем снова сериализовать его, подпись исходного сообщения может перестать сходиться. Маршрут вебхука должен получить raw body до преобразований фреймворка.

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

Повторная доставка — нормальная ситуация

Представьте, что обработчик сохранил событие, но ответ потерялся в сети. Отправитель не знает, завершился ли приём. Повторная попытка может принести те же данные ещё раз. Если на каждое обращение создавать новую задачу, один документ запустит несколько одинаковых обработок.

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

Идемпотентность означает, что повтор одного действия не меняет результат после первого успешного выполнения. Для приёма полезен уникальный ключ вида (provider, event_id). В базе он должен защищаться уникальным ограничением: последовательность «сначала SELECT, потом INSERT» без такой защиты допускает гонку двух запросов.

Есть и второй уровень: разные события могут относиться к одной бизнес-операции. Поэтому работник очереди тоже должен проверять состояние объекта. Например, уже обработанный документ не нужно обрабатывать заново только потому, что пришло ещё одно уведомление о его готовности.

Когда отвечать 200 и зачем нужна очередь

Приём лучше отделить от долгой работы. Обработчик проверяет запрос и фиксирует событие в надёжном хранилище, а отдельный работник выполняет обработку документа. Удобный вариант — таблица входящих событий, которая одновременно служит очередью: после успешного коммита можно подтвердить приём, даже если работник пока не запущен.

Проверить подпись и схему события
В транзакции сохранить событие с уникальным ключом
Если оно уже сохранено — вернуть успешный ответ
После коммита — подтвердить приём
Работник забирает необработанные записи и выполняет задачу

Если используется внешняя очередь, продумайте разрыв между записью в базу и отправкой задания. Иначе событие можно отметить принятым, но потерять задачу при сбое. Один из способов связать эти этапы — transactional outbox: вместе с событием сохраняется запись на последующую отправку.

Ответ 2xx подтверждает приём по договорённости сторон, а не обязательно готовность результата. Если сохранить событие не удалось, не выдавайте успешный ответ. Для подтверждённого дубля он обычно уместен: работа уже принята. Конкретные коды ошибок и условия повторов сверяйте с контрактом отправителя.

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

Прогоните сценарии: корректное событие, повтор, два одновременных запроса, испорченная подпись, недоступная база и падение работника после начала обработки. Отдельно проверьте старое событие, пришедшее после нового: нельзя безусловно перезаписывать актуальное состояние устаревшим уведомлением.

В журнале полезны идентификатор события, источник, время приёма, статус обработки и число попыток. Секреты и полное содержимое документов туда не нужны. Установите ограничения размера тела и времени обработки запроса; для ошибок работника предусмотрите повтор и очередь проблемных задач с уведомлением ответственному.

Рабочая интеграция заканчивается понятным маршрутом восстановления: где увидеть потерянную доставку, как запросить её повтор и как убедиться, что результат не продублировался. Тогда вебхук становится надёжным входом в процесс, а не просто URL, на который иногда приходит JSON.

Частые вопросы

Q.Что такое webhook простыми словами?

Это HTTP-уведомление, которое внешний сервис сам отправляет вашему приложению после события. Например, сообщает, что документ готов. Адрес приёма регистрируется у отправителя заранее.

Q.Чем webhook отличается от API?

Вебхук часто является частью API-интеграции. При обычном запросе ваше приложение обращается к сервису, а при вебхуке сервис инициирует доставку события. После уведомления подробности можно получить отдельным запросом к API.

Q.Все ли сервисы автоматически повторяют неуспешные вебхуки?

Нет. Stripe описывает автоматические повторы в рабочем режиме, а GitHub не пересылает неуспешные доставки автоматически. Порядок повторов, сроки и условия подтверждения нужно проверять в документации конкретного отправителя.

Q.Как защититься от повторной обработки события?

Сохраняйте идентификатор события вместе с источником под уникальным ограничением в базе. Работник очереди также должен учитывать состояние бизнес-операции. Памяти одного процесса недостаточно для надёжной защиты после перезапуска.

Источники

Поделиться:OKVK