Webhook — это когда один сервис сам стучится к вам, вместо того чтобы ждать, пока вы его спросите. Звучит просто, но именно здесь разработчики чаще всего теряют данные, получают дубли или кладут сервер.

Разберём всё по порядку: что такое webhook, как его правильно принять, что проверять, и как самому отправлять события в сторонние сервисы.

Чем webhook отличается от обычного API-запроса

Когда вы делаете запрос к API — вы инициатор. Вы спрашиваете: «Есть новые заказы?» — сервер отвечает. Это polling, и он плохо масштабируется: чтобы узнавать о событиях быстро, нужно опрашивать сервер каждые несколько секунд. Это лишняя нагрузка и задержка.

Webhook переворачивает логику. Вы говорите сервису: «Когда что-то произойдёт — пришли POST-запрос вот на этот URL». Сервис сам уведомляет вас в момент события. Никакого опроса, минимальная задержка.

Практический пример: платёжная система Stripe присылает webhook, когда клиент оплатил заказ. Вы получаете запрос, обновляете статус заказа в базе, отправляете письмо клиенту. Всё происходит за секунды, без ручного опроса.

Как выглядит входящий webhook

Вебхук — это обычный HTTP POST-запрос с JSON-телом. Вот типичный payload от той же Stripe:

{
  "id": "evt_1OqX2K2eZvKYlo2C",
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_3OqX2K2eZvKYlo2C",
      "amount": 4900,
      "currency": "rub",
      "status": "succeeded"
    }
  },
  "created": 1712345678
}

Ваша задача — принять этот запрос, проверить его подлинность и обработать. Звучит просто, но дьявол в деталях.

Шаг 1 — Сразу отвечайте 200 OK

Первое правило: endpoint должен ответить 200 OK как можно быстрее — в идеале за 1–3 секунды. Если вы не ответили за таймаут (обычно 5–30 секунд в зависимости от сервиса), отправитель считает доставку неудачной и попробует снова.

Не делайте тяжёлую работу синхронно. Правильная схема:

  1. Принять запрос
  2. Проверить подпись
  3. Положить в очередь
  4. Ответить 200 OK
  5. Обработать асинхронно

Если вы начнёте отправлять email, обращаться к базе и вызывать сторонние API прямо в обработчике webhook — рискуете получить таймаут и лавину повторных запросов.

@app.route('/webhook/stripe', methods=['POST'])
def stripe_webhook():
    payload = request.get_data()
    sig = request.headers.get('Stripe-Signature')
    
    # Проверяем подпись
    try:
        event = stripe.Webhook.construct_event(payload, sig, STRIPE_SECRET)
    except ValueError:
        return '', 400
    except stripe.error.SignatureVerificationError:
        return '', 400
    
    # Кладём в очередь и уходим
    queue.push(event)
    return '', 200

Шаг 2 — Проверяйте подпись

Webhook приходит из интернета. Без проверки подлинности любой может прислать поддельный запрос на ваш endpoint и сделать вид, что заказ оплачен.

Большинство сервисов подписывают webhook через HMAC-SHA256. Они берут тело запроса, добавляют секретный ключ и считают хэш. Вы делаете то же самое со своим ключом и сравниваете.

Пример для GitHub:

import hmac
import hashlib

def verify_github_signature(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    expected_header = f'sha256={expected}'
    return hmac.compare_digest(expected_header, signature)

Важно: используйте hmac.compare_digest вместо обычного ==. Это защита от timing attack — атаки по времени, при которой злоумышленник угадывает подпись по скорости ответа.

Если сервис не предоставляет подписи — добавляйте секретный токен в URL: /webhook/payment?token=your_secret_token. Не идеально, но лучше, чем ничего.

Шаг 3 — Обеспечьте идемпотентность

Вебхуки доставляются по принципу «at least once» — хотя бы один раз. Это значит, что один и тот же webhook может прийти дважды: сервер получил ваш 200 OK, но решил переотправить из-за сетевой проблемы. Или вы вернули 500 и получили несколько повторов.

Если ваш обработчик не идемпотентен, вы дважды спишете деньги, дважды отправите письмо, создадите дубль заказа.

Решение: сохраняйте ID события и проверяйте его перед обработкой.

def handle_payment(event):
    event_id = event['id']
    
    # Проверяем, не обрабатывали ли уже
    if ProcessedEvent.exists(event_id):
        return  # Пропускаем дубль
    
    # Обрабатываем
    process_payment(event['data']['object'])
    
    # Сохраняем ID
    ProcessedEvent.create(event_id=event_id)

ID события уникален — это гарантирует отправитель. Таблица processed_events с уникальным индексом на event_id — простое и надёжное решение.

Шаг 4 — Логируйте всё входящее

Webhook — это внешнее событие. Вы не можете его воспроизвести по требованию. Если что-то пошло не так, вам нужна история.

Сохраняйте:

  • Полное тело запроса (raw)
  • Заголовки
  • Время получения
  • Статус обработки
  • Ошибки, если были

Это не параноя, это практика. Когда клиент говорит «заказ оплачен, но ничего не произошло» — вы открываете логи и видите, что webhook пришёл, но упал на этапе отправки email из-за неправильного SMTP.

Как самому отправлять webhook

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

Основные принципы:

Повторные попытки с экспоненциальной задержкой. Если получатель вернул ошибку или не ответил — повторите через 10 секунд, потом через минуту, через 5 минут, через час. Не бомбардируйте сервер мгновенными повторами.

RETRY_DELAYS = [10, 60, 300, 3600, 86400]  # секунды

def send_with_retry(url, payload, attempt=0):
    try:
        response = requests.post(url, json=payload, timeout=10)
        if response.status_code == 200:
            return True
    except requests.RequestException:
        pass
    
    if attempt < len(RETRY_DELAYS):
        delay = RETRY_DELAYS[attempt]
        schedule_retry(url, payload, attempt + 1, delay)
    else:
        mark_webhook_failed(url, payload)

Подписывайте исходящие вебхуки. Даже если получатель не проверяет — это хорошая практика. Добавьте заголовок с HMAC-подписью.

Давайте уникальный ID каждому событию. Получатель должен иметь возможность дедуплицировать.

Не отправляйте вебхук синхронно. Всегда через очередь. Иначе медленный получатель замедляет вашу систему.

Типичные ошибки

Обрабатывать синхронно. Уже упомянули, но повторим: долгая обработка = таймаут = повтор = хаос.

Не проверять Content-Type. Убедитесь, что принимаете application/json и парсите тело правильно. Некоторые сервисы присылают application/x-www-form-urlencoded.

Доверять данным из payload без верификации. Проверяйте подпись всегда, а критические данные (сумму платежа, статус) дополнительно запрашивайте через API отправителя.

Открытый endpoint без аутентификации. Минимум — проверка подписи или секретного токена.

Игнорировать версионирование. Некоторые сервисы меняют структуру webhook со временем. Указывайте версию API при подписке и обрабатывайте смену версии аккуратно.

Инструменты для отладки

Webhook сложно тестировать локально — сторонний сервис не может достучаться до localhost. Вот что помогает:

ngrok — создаёт публичный URL, который проксирует трафик на ваш локальный порт. Запустил ngrok http 8000 — получил https://abc123.ngrok.io, указал его в настройках сервиса.

webhook.site — онлайн-инспектор. Даёт уникальный URL, на который можно отправлять вебхуки и смотреть их содержимое в реальном времени. Полезно, чтобы изучить формат перед реализацией.

Stripe CLI, GitHub CLI — у крупных сервисов есть свои инструменты для локального тестирования вебхуков. Stripe CLI умеет форвардить события на localhost и повторять их.

Когда webhook — не лучший выбор

Webhook хорош для асинхронных событий, где важна доставка в реальном времени. Но он не универсален:

  • Если нужно получить данные прямо сейчас — используйте обычный API-запрос
  • Если событий очень много и сторона-получатель не справляется — лучше polling с батчингом
  • Если нужна двусторонняя связь в реальном времени — смотрите в сторону WebSocket

Webhook — инструмент для уведомлений. Не пытайтесь строить на нём RPC или стриминг данных.

Безопасность endpoint

Несколько практических мер:

  • Используйте HTTPS всегда. HTTP webhook — это передача данных открытым текстом
  • Ограничьте IP, если сервис публикует список своих адресов (Stripe, GitHub это делают)
  • Не раскрывайте в ответах подробности об ошибках — возвращайте просто 400 или 500
  • Ставьте rate limiting на endpoint, чтобы защититься от флуда

Если вы заказываете разработку интеграции с внешними сервисами, убедитесь, что подрядчик реализует все эти проверки. В REEXY, например, интеграция сервисов начинается от 1 500 ₽, и в неё входит корректная обработка вебхуков — с подписью, идемпотентностью и логированием.

Мониторинг и алерты

Настройте метрики на своём endpoint:

  • Количество входящих вебхуков в минуту
  • Процент успешной обработки
  • Время обработки (p50, p95, p99)
  • Количество дублей

Если вы отправляете вебхуки — отслеживайте:

  • Количество неудачных доставок
  • Endpoint'ы с постоянными ошибками
  • Задержку до успешной доставки

Вебхук, который перестал доставляться, — это молчаливая потеря данных. Без мониторинга вы узнаете об этом только от клиента.

Итого: чеклист

При реализации входящего webhook:

  • Отвечаешь 200 OK немедленно, обрабатываешь асинхронно
  • Проверяешь HMAC-подпись
  • Сохраняешь ID события, проверяешь дубли
  • Логируешь raw payload и результат обработки
  • Endpoint закрыт по HTTPS, есть rate limiting

При реализации исходящего webhook:

  • Отправляешь через очередь
  • Реализуешь повторы с экспоненциальной задержкой
  • Подписываешь payload
  • Присваиваешь уникальный ID каждому событию
  • Мониторишь доставку

Webhook — это не сложно, если сделать правильно с первого раза. Большинство проблем возникают именно потому, что на старте экономят на проверках и очередях, а потом разгребают потерянные события и дубли в продакшне.