Что такое Webhook: как работает и как реализовать

← Вернуться к статьям

Введение

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

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

В этой статье вы узнаете:

📋 Содержание

Что такое Webhook

Webhook (вебхук) — это HTTP-callback: автоматический HTTP-запрос, который отправляется на указанный вами URL, когда на стороне сервиса происходит определённое событие.

Простая аналогия:

API (polling) — вы каждые 5 минут звоните в пиццерию: «Мой заказ готов?» «Нет». «А сейчас?» «Нет». «А сейчас?»...

Webhook — вы оставляете номер телефона, и пиццерия сама звонит вам, когда пицца готова.

Webhook — это «обратный API-вызов». Вместо того чтобы вы спрашивали сервис о событиях, сервис сам уведомляет вас:

Как работает Webhook

1. Настройка: Вы указываете URL в настройках сервиса
2. Событие: В сервисе происходит событие (оплата, push, сообщение)
3. Отправка: Сервис отправляет POST-запрос на ваш URL
4. Обработка: Ваш сервер получает данные и обрабатывает
5. Ответ: Ваш сервер отвечает 200 OK

Пошаговый процесс на примере платёжной системы:

1
Регистрация Webhook
В панели Stripe вы указываете URL: https://myapp.com/webhooks/stripe и выбираете события: payment_intent.succeeded, payment_intent.failed.
2
Событие происходит
Клиент оплачивает заказ на $99. Stripe обрабатывает платёж.
3
Stripe отправляет POST-запрос
POST https://myapp.com/webhooks/stripe с JSON-телом, содержащим данные о платеже.
4
Ваш сервер обрабатывает
Обновляет статус заказа, отправляет письмо клиенту, активирует подписку.
5
Ваш сервер отвечает 200 OK
Stripe получает подтверждение. Если ваш сервер не ответит — Stripe повторит попытку.

Пример Webhook-запроса от Stripe

POST /webhooks/stripe HTTP/1.1
Host: myapp.com
Content-Type: application/json
Stripe-Signature: t=1708700000,v1=5257a869e7...

{
  "id": "evt_1234567890",
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_abcdef",
      "amount": 9900,
      "currency": "usd",
      "status": "succeeded",
      "customer": "cus_xyz",
      "metadata": {
        "order_id": "ORD-42"
      }
    }
  },
  "created": 1708700000
}

Webhook vs Polling

Два способа узнать о событиях на стороне внешнего сервиса:

Характеристика Webhook (Push) Polling (Pull)
Кто инициирует Сервис → ваш сервер Ваш сервер → сервис
Задержка Мгновенная (секунды) Зависит от интервала (минуты)
Нагрузка на сеть Минимальная (только при событиях) Постоянная (даже без событий)
Сложность реализации Средняя (нужен публичный URL) Низкая (простой цикл запросов)
Надёжность Нужен retry-механизм Автоматически при следующем цикле
Требует публичного URL Да Нет

✅ Когда Webhook

  • Нужна мгновенная реакция (платежи, уведомления)
  • События происходят непредсказуемо и редко
  • Важно снизить нагрузку на API
  • Сервис поддерживает Webhook'и

❌ Когда Polling

  • Нет публичного URL (мобильные, десктоп)
  • Сервис не поддерживает Webhook'и
  • Нужна гарантированная доставка
  • Простая интеграция без инфраструктуры

Примеры из реальной жизни

Сервис Событие Что делать при получении
Stripe payment_intent.succeeded Обновить статус заказа, отправить чек
GitHub push Запустить CI/CD pipeline, деплой
Telegram Новое сообщение Ответить через бота
Shopify orders/create Передать заказ в CRM, отправить в склад
Slack Slash-команда Выполнить действие, отправить ответ
Jira Issue updated Синхронизировать статус с трекером

Реализация обработчика

Рассмотрим полную реализацию Webhook-обработчика на трёх языках.

Node.js (Express)

const express = require('express');
const crypto = require('crypto');
const app = express();

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

app.post('/webhooks/payment', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const payload = req.body;

  // 1. Проверяем подпись
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  if (signature !== expectedSignature) {
    console.error('Невалидная подпись');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. Парсим тело
  const event = JSON.parse(payload);

  // 3. Отвечаем 200 СРАЗУ (до обработки!)
  res.status(200).json({ received: true });

  // 4. Обрабатываем асинхронно
  processWebhook(event).catch(err => {
    console.error('Ошибка обработки webhook:', err);
  });
});

async function processWebhook(event) {
  switch (event.type) {
    case 'payment.succeeded':
      await updateOrderStatus(event.data.order_id, 'paid');
      await sendReceiptEmail(event.data.customer_email);
      break;

    case 'payment.failed':
      await updateOrderStatus(event.data.order_id, 'failed');
      await notifyAdmin(event.data);
      break;

    case 'subscription.cancelled':
      await deactivateSubscription(event.data.subscription_id);
      break;

    default:
      console.log(`Неизвестный тип события: ${event.type}`);
  }
}

app.listen(3000);

Python (Flask)

import hmac
import hashlib
import json
from flask import Flask, request, jsonify
from threading import Thread

app = Flask(__name__)
WEBHOOK_SECRET = 'your-secret-key'

@app.route('/webhooks/payment', methods=['POST'])
def handle_webhook():
    # 1. Проверяем подпись
    signature = request.headers.get('X-Webhook-Signature', '')
    payload = request.get_data()

    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        return jsonify({'error': 'Invalid signature'}), 401

    # 2. Парсим тело
    event = json.loads(payload)

    # 3. Отвечаем 200 сразу
    # 4. Обрабатываем в фоне
    Thread(target=process_webhook, args=(event,)).start()

    return jsonify({'received': True}), 200

def process_webhook(event):
    event_type = event.get('type')

    if event_type == 'payment.succeeded':
        order_id = event['data']['order_id']
        print(f'Оплата получена для заказа {order_id}')
        # update_order_status(order_id, 'paid')
        # send_receipt_email(event['data']['customer_email'])

    elif event_type == 'payment.failed':
        print(f'Оплата не прошла: {event["data"]}')
        # notify_admin(event['data'])

    else:
        print(f'Неизвестное событие: {event_type}')

if __name__ == '__main__':
    app.run(port=3000)

PHP (Laravel)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class WebhookController extends Controller
{
    public function handlePayment(Request $request)
    {
        // 1. Проверяем подпись
        $signature = $request->header('X-Webhook-Signature');
        $payload   = $request->getContent();
        $secret    = config('services.webhook.secret');

        $expected = hash_hmac('sha256', $payload, $secret);

        if (!hash_equals($expected, $signature)) {
            Log::warning('Webhook: невалидная подпись');
            return response()->json(['error' => 'Invalid signature'], 401);
        }

        // 2. Парсим
        $event = json_decode($payload, true);

        // 3. Отвечаем 200 сразу
        // Обработку ставим в очередь
        dispatch(new \App\Jobs\ProcessWebhook($event));

        return response()->json(['received' => true], 200);
    }
}

// app/Jobs/ProcessWebhook.php
class ProcessWebhook implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(private array $event) {}

    public function handle(): void
    {
        match ($this->event['type']) {
            'payment.succeeded' => $this->handlePaymentSuccess(),
            'payment.failed'    => $this->handlePaymentFailed(),
            default             => Log::info("Неизвестное событие: {$this->event['type']}"),
        };
    }

    private function handlePaymentSuccess(): void
    {
        $orderId = $this->event['data']['order_id'];
        Order::where('id', $orderId)->update(['status' => 'paid']);
        // Mail::to(...)->send(new OrderPaidMail(...));
    }

    private function handlePaymentFailed(): void
    {
        Log::error('Оплата не прошла', $this->event['data']);
    }
}

⚠️ Золотое правило: отвечайте 200 быстро!

Webhook-отправитель ждёт ответа 5–30 секунд. Если не получит 200 OK вовремя — считает доставку неудачной и повторит попытку. Поэтому сначала отвечайте, а потом обрабатывайте данные асинхронно (очереди, фоновые потоки).

Безопасность Webhook'ов

Webhook endpoint — это публичный URL. Любой может отправить на него поддельный запрос. Без верификации злоумышленник может:

Проверка подписи (HMAC)

Стандартный механизм верификации:

1
Отправитель вычисляет HMAC-SHA256 от тела запроса с секретным ключом и помещает результат в заголовок (X-Webhook-Signature).
2
Получатель повторяет вычисление с тем же секретным ключом и сравнивает подписи. Совпали — запрос подлинный.
// Проверка подписи (Node.js)
function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // timing-safe сравнение для защиты от timing attack
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

⚠️ Дополнительные меры безопасности

Retry-логика и идемпотентность

Если ваш сервер не ответил 200 OK, отправитель повторит Webhook. Это значит, что один и тот же Webhook может прийти несколько раз.

Типичная retry-стратегия

Попытка Задержка Сервис
1-я (повтор) 1 минута Stripe, GitHub
2-я 5 минут Stripe, Shopify
3-я 30 минут Stripe
4-я 2 часа Stripe
5-я 24 часа Stripe
Деактивация После 3–7 дней неудач Все

Идемпотентная обработка

Ваш обработчик должен быть идемпотентным: повторная обработка того же Webhook'а не должна создавать дубликаты.

async function processWebhook(event) {
  // Проверяем, обрабатывали ли уже это событие
  const exists = await db.webhookEvents.findOne({
    eventId: event.id
  });

  if (exists) {
    console.log(`Событие ${event.id} уже обработано, пропускаем`);
    return;
  }

  // Обрабатываем
  await handleEvent(event);

  // Сохраняем ID обработанного события
  await db.webhookEvents.insertOne({
    eventId: event.id,
    type: event.type,
    processedAt: new Date()
  });
}

Правила идемпотентности:

  1. Сохраняйте ID события — в БД или Redis
  2. Проверяйте перед обработкой — «уже обрабатывали?»
  3. Используйте транзакции — обработка + сохранение ID атомарно
  4. TTL для записей — чистите старые ID (через 7 дней)

Отладка Webhook'ов

Главная сложность: Webhook приходит от внешнего сервиса на ваш сервер. На localhost внешний сервис отправить запрос не может. Вот как это решить:

1. ngrok — туннель на localhost

# Установить ngrok
brew install ngrok   # macOS
# или скачайте с ngrok.com

# Запустить туннель
ngrok http 3000

# Получите публичный URL:
# https://abc123.ngrok-free.app → localhost:3000

# Укажите этот URL как Webhook в настройках сервиса:
# https://abc123.ngrok-free.app/webhooks/stripe

2. webhook.site — просмотр запросов

webhook.site даёт уникальный URL и показывает все входящие запросы: заголовки, тело, метод. Идеально для начального изучения формата Webhook'ов без написания кода.

3. Replay — повторная отправка

Многие сервисы (Stripe, GitHub, Shopify) позволяют повторно отправить Webhook из дашборда. Используйте эту функцию для отладки.

4. CLI-инструменты

# Stripe CLI — отладка Stripe Webhook'ов локально
stripe listen --forward-to localhost:3000/webhooks/stripe

# GitHub CLI — тестирование GitHub Webhook'ов
# Используйте раздел Settings → Webhooks → Recent Deliveries → Redeliver

Best Practices

✅ Чек-лист реализации Webhook

  1. Отвечайте 200 мгновенно — обработку выполняйте асинхронно
  2. Проверяйте подпись — HMAC-SHA256 с timing-safe сравнением
  3. Обеспечьте идемпотентность — храните ID обработанных событий
  4. Используйте HTTPS — никогда не принимайте Webhook'и по HTTP
  5. Логируйте всё — полный запрос для отладки проблем
  6. Настройте мониторинг — алерт при росте ошибок обработки
  7. Используйте очередь — RabbitMQ, Redis Queue, SQS для надёжности
  8. Обрабатывайте все события — даже неизвестные (логируйте и пропускайте)

❌ Частые ошибки

Архитектура для production

Webhook → Ваш Endpoint → [Validate Signature] → [Save to Queue] → 200 OK
                                                        ↓
                                                  [Queue Worker]
                                                        ↓
                                              [Process Event]
                                                        ↓
                                          [Update DB / Send Email / ...]

⚠️ Порядок событий не гарантирован

Webhook invoice.paid может прийти раньше invoice.created. Ваша система должна корректно обрабатывать события в любом порядке. Используйте timestamp события и текущее состояние ресурса в БД, а не полагайтесь на порядок получения.

FAQ

❓ Чем Webhook отличается от API?

Ответ: API работает по модели «запрос-ответ»: клиент сам спрашивает сервер. Webhook — наоборот: сервер сам отправляет данные клиенту, когда происходит событие. API — «вы звоните», Webhook — «вам звонят».

❓ Что произойдёт, если Webhook-сервер недоступен?

Ответ: Большинство сервисов реализуют retry-механизм: при ошибке отправка повторяется с экспоненциальной задержкой (1 мин → 5 мин → 30 мин → 2 часа). После нескольких дней неудач Webhook деактивируется. Поэтому важно:

  • Отвечать 200 OK быстро (до обработки)
  • Иметь мониторинг доступности
  • Обеспечить идемпотентность (retry = дубликаты)

❓ Как проверить подлинность Webhook-запроса?

Ответ: Стандартный способ — HMAC-SHA256 подпись:

  1. Отправитель вычисляет хеш от тела запроса с секретным ключом
  2. Результат помещается в HTTP-заголовок
  3. Получатель повторяет вычисление и сравнивает

Используйте timing-safe сравнение (crypto.timingSafeEqual) для защиты от timing-атак.

❓ Как отлаживать Webhook локально?

Ответ: Три способа:

  • ngrok — создаёт публичный URL-туннель на localhost
  • webhook.site — показывает входящие запросы без кода
  • Stripe CLI / GitHub CLI — пересылают Webhook'и на localhost

Заключение

📝 Ключевые тезисы

Аспект Рекомендация
Ответ 200 OK мгновенно, обработка асинхронно
Безопасность HMAC-SHA256, HTTPS, проверка timestamp
Идемпотентность Хранить ID обработанных событий
Архитектура Endpoint → Queue → Worker
Отладка ngrok для localhost, webhook.site для просмотра
Мониторинг Логирование, алерты на ошибки, дашборд

Webhook'и — это мост между вашим приложением и внешним миром. Правильная реализация делает интеграции надёжными, быстрыми и безопасными. Неправильная — приводит к потерянным платежам, дублированным заказам и бессонным ночам.

💡 Три главных правила:

  1. Отвечайте быстро — 200 OK за миллисекунды, обработка в фоне
  2. Проверяйте подпись — никогда не доверяйте входящему запросу без верификации
  3. Будьте идемпотентными — Webhook придёт повторно, не создавайте дубликаты

🎯 Тестируйте Webhook'и с LightBox API

Нужно протестировать, как ваше приложение обрабатывает Webhook'и?

LightBox API позволяет создать Mock-endpoint, который будет отправлять настраиваемые Webhook-запросы на ваш сервер — с любыми заголовками, телом и задержкой.

Попробовать бесплатно →

Статья опубликована: 23 февраля 2026
Автор: LightBox API Team