Webhooks — основа уведомлений о платежах в реальном времени. Это руководство объясняет, как защитить ваш webhook endpoint.
Понимание подписей Webhook
Каждый webhook от Pulse2Pay включает два заголовка безопасности:
|-----------|----------|
X-Pulse2Pay-SignatureX-Pulse2Pay-TimestampПроверка подписи
Важно: Подписи webhook используют другой формат, чем подписи API запросов!
// Подпись API (для ваших запросов к Pulse2Pay):
HMAC-SHA256(api_secret, timestamp + "." + method + "." + path + "." + body)
// Подпись Webhook (для запросов Pulse2Pay к вам):
HMAC-SHA256(webhook_secret, timestamp + "." + body)
Секрет webhook предоставляется при регистрации endpoint.
Пример реализации (Node.js)
const crypto = require('crypto');function verifyWebhookSignature(req, webhookSecret) {
const signature = req.headers['x-pulse2pay-signature'];
const timestamp = req.headers['x-pulse2pay-timestamp'];
// Используйте сырое тело для точной проверки подписи
const body = req.rawBody || JSON.stringify(req.body);
// Проверка timestamp в пределах 5 минут (timestamp в миллисекундах)
const now = Date.now();
if (Math.abs(now - parseInt(timestamp)) > 300000) {
throw new Error('Timestamp устарел - возможная replay атака');
}
// Вычисление ожидаемой подписи (формат webhook: timestamp.body)
const payload = ${timestamp}.${body};
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
throw new Error('Неверная подпись');
}
return true;
}
Логика повторов
Pulse2Pay повторяет неудачные webhooks с экспоненциальной задержкой:
|---------|----------|
После 5 неудачных попыток webhook будет помечен как failed.
Идемпотентная обработка
Ваш обработчик должен быть идемпотентным. Один webhook может быть доставлен несколько раз.
async function handlePaymentWebhook(payment) {
const existing = await db.getPayment(payment.id);
if (existing && existing.status === payment.status) {
return { success: true, duplicate: true };
}
await db.updatePayment(payment.id, {
status: payment.status,
tx_hash: payment.tx_hash,
processed_at: new Date()
});
return { success: true };
}