Введение
Представьте ситуацию: вы интегрировали платёжную систему. Клиент оплатил заказ, но как ваш сервер узнает об этом? Опрашивать платёжку каждую секунду вопросом «оплатили?» — неэффективно. Гораздо лучше, если платёжная система сама сообщит вам об успешной оплате.
Именно для этого существуют Webhook'и — механизм, при котором внешний сервис сам отправляет HTTP-запрос на ваш сервер, когда происходит важное событие. Webhook — одна из ключевых концепций современной интеграционной разработки.
В этой статье вы узнаете:
- ✅ Что такое Webhook и как он работает
- ✅ Отличие Webhook от API polling
- ✅ Реализацию обработчика на Node.js, Python и PHP
- ✅ Безопасность: верификация подписи
- ✅ Retry-логика и обработка ошибок
- ✅ Как отлаживать Webhook'и локально
📋 Содержание
Что такое Webhook
Webhook (вебхук) — это HTTP-callback: автоматический HTTP-запрос, который отправляется на указанный вами URL, когда на стороне сервиса происходит определённое событие.
Простая аналогия:
API (polling) — вы каждые 5 минут звоните в пиццерию: «Мой заказ готов?» «Нет». «А сейчас?» «Нет». «А сейчас?»...
Webhook — вы оставляете номер телефона, и пиццерия сама звонит вам, когда пицца готова.
Webhook — это «обратный API-вызов». Вместо того чтобы вы спрашивали сервис о событиях, сервис сам уведомляет вас:
- Stripe уведомляет о платеже
- GitHub уведомляет о push в репозиторий
- Telegram отправляет новое сообщение в бота
- Shopify уведомляет о новом заказе
Как работает Webhook
2. Событие: В сервисе происходит событие (оплата, push, сообщение)
3. Отправка: Сервис отправляет POST-запрос на ваш URL
4. Обработка: Ваш сервер получает данные и обрабатывает
5. Ответ: Ваш сервер отвечает 200 OK
Пошаговый процесс на примере платёжной системы:
В панели Stripe вы указываете URL:
https://myapp.com/webhooks/stripe и выбираете события: payment_intent.succeeded, payment_intent.failed.
Клиент оплачивает заказ на $99. Stripe обрабатывает платёж.
POST https://myapp.com/webhooks/stripe с JSON-телом, содержащим данные о платеже.
Обновляет статус заказа, отправляет письмо клиенту, активирует подписку.
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)
Стандартный механизм верификации:
X-Webhook-Signature).
// Проверка подписи (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)
);
}
⚠️ Дополнительные меры безопасности
- HTTPS обязательно — Webhook URL должен быть только HTTPS
- Проверяйте timestamp — отклоняйте запросы старше 5 минут (защита от replay attack)
- Whitelisting IP — если сервис публикует IP-адреса, ограничьте доступ по ним
- Не доверяйте данным — перепроверяйте критичные данные (сумма платежа) через API сервиса
- Timing-safe сравнение — используйте
crypto.timingSafeEqualилиhmac.compare_digest
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()
});
}
Правила идемпотентности:
- Сохраняйте ID события — в БД или Redis
- Проверяйте перед обработкой — «уже обрабатывали?»
- Используйте транзакции — обработка + сохранение ID атомарно
- 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
- Отвечайте 200 мгновенно — обработку выполняйте асинхронно
- Проверяйте подпись — HMAC-SHA256 с timing-safe сравнением
- Обеспечьте идемпотентность — храните ID обработанных событий
- Используйте HTTPS — никогда не принимайте Webhook'и по HTTP
- Логируйте всё — полный запрос для отладки проблем
- Настройте мониторинг — алерт при росте ошибок обработки
- Используйте очередь — RabbitMQ, Redis Queue, SQS для надёжности
- Обрабатывайте все события — даже неизвестные (логируйте и пропускайте)
❌ Частые ошибки
- Долгая обработка — тяжёлая логика в обработчике, таймаут → retry → дубликаты
- Нет проверки подписи — любой может отправить поддельный Webhook
- Нет идемпотентности — retry создаёт дубликаты (два платежа, два письма)
- HTTP вместо HTTPS — данные перехватываются в открытом виде
- Нет логирования — невозможно понять, что пошло не так
- Жёсткая зависимость от формата — сервис добавляет новое поле → ваш парсинг ломается
Архитектура для 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 подпись:
- Отправитель вычисляет хеш от тела запроса с секретным ключом
- Результат помещается в HTTP-заголовок
- Получатель повторяет вычисление и сравнивает
Используйте 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'и — это мост между вашим приложением и внешним миром. Правильная реализация делает интеграции надёжными, быстрыми и безопасными. Неправильная — приводит к потерянным платежам, дублированным заказам и бессонным ночам.
💡 Три главных правила:
- Отвечайте быстро — 200 OK за миллисекунды, обработка в фоне
- Проверяйте подпись — никогда не доверяйте входящему запросу без верификации
- Будьте идемпотентными — Webhook придёт повторно, не создавайте дубликаты
🎯 Тестируйте Webhook'и с LightBox API
Нужно протестировать, как ваше приложение обрабатывает Webhook'и?
LightBox API позволяет создать Mock-endpoint, который будет отправлять настраиваемые Webhook-запросы на ваш сервер — с любыми заголовками, телом и задержкой.
- ✓ Отправка тестовых Webhook'ов на любой URL
- ✓ Настройка заголовков, тела и статус кодов
- ✓ Логирование всех запросов и ответов
- ✓ Бесплатный план для старта
Статья опубликована: 23 февраля 2026
Автор: LightBox API Team