Skip to main content

Верификация подписи вебхуков

Для защиты от поддельных запросов каждый вебхук подписывается с помощью HMAC-SHA256. Заголовок x-webhook-signature содержит подпись в формате sha256=xxx.

Заголовки вебхука

x-webhook-signature: sha256=abc123def456...
x-webhook-id: webhook-uuid
x-webhook-timestamp: 1705312200000
Content-Type: application/json
User-Agent: LovePay/1.0

Где найти секретный ключ

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

Алгоритм верификации

  1. Получите заголовок x-webhook-signature из запроса
  2. Извлеките хеш из строки (уберите префикс sha256=)
  3. Вычислите HMAC-SHA256 от raw body с вашим секретным ключом
  4. Сравните подписи (используйте timing-safe сравнение)
Важно: Используйте RAW body запроса, а не распарсенный JSON! Парсинг может изменить форматирование и подпись не совпадёт.

Примеры реализации

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

const app = express();

// Важно: используйте raw body для верификации
app.use('/webhooks', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.LOVEANDPAY_WEBHOOK_SECRET;

function verifySignature(rawBody, signatureHeader, secret) {
  // Подпись приходит в формате "sha256=xxx"
  const signature = signatureHeader.replace('sha256=', '');

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)  // Используйте RAW body, не parsed JSON!
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhooks/loveandpay', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const rawBody = req.body.toString();

  // Верифицируем подпись
  if (!verifySignature(rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Обрабатываем событие
  const payload = JSON.parse(rawBody);
  console.log('Event:', payload.event, 'Data:', payload.data);

  switch (payload.event) {
    case 'INVOICE_PAID':
      handleInvoicePaid(payload.data);
      break;
    case 'INVOICE_EXPIRED':
      handleInvoiceExpired(payload.data);
      break;
    // ... другие события
  }

  res.json({ success: true });
});

function handleInvoicePaid(data) {
  console.log(`Счёт ${data.id} оплачен!`);
  // Ваша логика: обновление заказа, отправка товара и т.д.
}

function handleInvoiceExpired(data) {
  console.log(`Счёт ${data.id} истёк`);
  // Ваша логика: уведомление клиента, отмена заказа и т.д.
}

app.listen(3000);

Важные замечания

Подпись передаётся в заголовке x-webhook-signature в формате sha256=xxx. Перед сравнением уберите префикс sha256=.
Всегда используйте функции для безопасного сравнения строк (crypto.timingSafeEqual, hmac.compare_digest, hash_equals), чтобы предотвратить timing-атаки.
Для верификации обязательно используйте оригинальное тело запроса до парсинга JSON. Некоторые фреймворки автоматически парсят JSON, что может изменить форматирование и подпись не совпадёт.
События могут приходить повторно (см. поле retryCount). Используйте id события для дедупликации и предотвращения повторной обработки.
ID счета передается в поле data.id, а не data.invoiceId. Учитывайте это при обработке событий.

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

Редирект HTTP → HTTPS (код 301)

Распространённая проблема: Если ваш сервер (nginx) настроен на редирект с HTTP на HTTPS, вебхуки не будут работать!При 301 редиректе POST-запрос автоматически превращается в GET — это стандартное поведение HTTP протокола (RFC 7231).
Пример неправильного URL:
http://api.example.com/webhook
Правильный URL:
https://api.example.com/webhook
Решение: Всегда указывайте URL вебхука с протоколом HTTPS в настройках. Либо настройте nginx на использование кода 307 (Temporary Redirect), который сохраняет метод запроса.
# Вместо 301 используйте 307 для сохранения POST-метода
server {
    listen 80;
    server_name api.example.com;
    return 307 https://$server_name$request_uri;
}

Тестирование вебхуков

Для локальной разработки используйте туннели:
# ngrok
ngrok http 3000

# localtunnel
lt --port 3000
Затем укажите полученный URL при создании вебхука в личном кабинете.