Webhook-уведомления
Уведомления об изменении статуса заявок и споров
Обзор
Система webhook-уведомлений Parallel обеспечивает надежную доставку событий об изменении статуса заявок на ваш endpoint.
События webhook
Типы событий
Webhook отправляются при следующих изменениях статуса заявки:
| Триггер | Когда отправляется |
|---|---|
| Заявка оплачена | Заявка на приём — после того как зачтён чек: зачтённая сумма в поле paidAmount, она меньше amount, если клиент перевёл меньше. Каждый следующий зачтённый чек до expireAt (доплата) — ещё одно уведомление paid с новым paidAmount. Выплата — после перевода получателю. Из expired и canceled заявка в paid не переходит, кроме как по решению апелляции |
| Истек срок | Заявку не оплатили до expireAt (срок задаётся для магазина): ни один чек не зачтён и ни один не проверяется. Выплату не выплатили целиком до newStatusExpiresAt (по умолчанию 5 часов) |
| Отменена | При отмене мерчантом или администратором |
| Отправлена на проверку | Чеки на всю сумму выплаты загружены и проверяются (только для direction: "out") |
| Чек: нужен доп | По чеку нужен доп: тело — объект чека со статусом proof_required |
| Чек зачтён | Тело — объект чека со статусом accepted |
| Чек отклонён | Тело — объект чека со статусом rejected |
| Создан спор | Открыт спор по заявке |
| Спор решен | Спор закрыт с решением |
Формат webhook запроса
HTTP запрос
POST {notificationUrl}
Content-Type: application/json
X-Webhook-Signature: {hmac_sha256_signature}
X-Webhook-Event: {event_type}
X-Webhook-Delivery-Id: {unique_delivery_id}Заголовки
| Заголовок | Описание |
|---|---|
X-Webhook-Signature | HMAC-SHA256 подпись тела запроса в hex-формате. Используйте для проверки подлинности webhook (см. Верификация подписи) |
X-Webhook-Event | Тип события (например, invoice.paid, invoice.expired, invoice.review). Позволяет быстро определить тип события без парсинга тела, но для безопасности обязательно сверяйте его со status в теле запроса |
X-Webhook-Delivery-Id | Уникальный ID доставки webhook (UUID). Повторная доставка приходит с тем же ID |
Retry стратегия
Автоматические повторы
Parallel автоматически повторяет неудачные доставки webhook:
| Попытка | Задержка | Условие |
|---|---|---|
| 1 | Немедленно | Первая попытка доставки |
| 2 | ~1 минута | Если первая попытка вернула не-2xx или timeout |
| 3 | ~5 минут | Если вторая попытка вернула не-2xx или timeout |
После 3 неудачных попыток webhook больше не отправляется. Итоговый статус можно получить через проверку статуса заявки или выплаты.
Важно: Webhook считается успешным только при ответе 200-299. Любой другой код (включая 3xx, 4xx, 5xx) вызовет retry.
Когда происходит retry
| Сценарий | Retry? | Примечания |
|---|---|---|
| HTTP 200-299 | ❌ Нет | Успех |
| HTTP 4xx | ✅ Да | Ваш endpoint может быть временно недоступен |
| HTTP 5xx | ✅ Да | Ошибка сервера |
| Timeout (>10s) | ✅ Да | Сетевая проблема |
| Connection refused | ✅ Да | Endpoint недоступен |
| DNS failure | ✅ Да | Проблемы DNS |
Тело webhook запроса
Для событий заявки
Тело — объект заявки с новым статусом:
{
"id": "cm3k8x7y80001z8j4k5m6n7o8",
"amount": "1000.0000",
"status": "paid",
"currency": "RUB",
"dealRate": "95.50",
"paidAmount": "1000.0000",
"expireAt": "2025-11-03T15:15:00+03:00",
"createdAt": "2025-11-03T15:00:00+03:00",
"direction": "in",
"updatedAt": "2025-11-03T15:05:00+03:00",
"requisiteId": "req_abc123xyz789",
"paymentMethod": "SBP",
"paymentOption": "sberbank",
"dealRequisites": "{\"bankName\":\"Сбербанк\",\"fullName\":\"Ivan I.\",\"phoneNumber\":\"79001234567\"}",
"internalId": "order-12345",
"merchantName": "MerchantName"
}Для событий чека
Тело — объект чека с новым статусом. Заголовок X-Webhook-Event: receipt.proof_required, receipt.accepted или receipt.rejected.
Для событий выплаты
Тело — объект выплаты с новым статусом. Реквизиты получателя в нём — объект, а не JSON-строка:
{
"id": "cm3k8x7y80001z8j4k5m6n7o8",
"amount": "1000.0000",
"status": "paid",
"currency": "RUB",
"dealRate": "95.50",
"newStatusExpiresAt": "2025-11-03T20:00:00+03:00",
"createdAt": "2025-11-03T15:00:00+03:00",
"direction": "out",
"updatedAt": "2025-11-03T15:05:00+03:00",
"paymentMethod": "TO_CARD",
"paymentOption": "tbank",
"dealRequisites": {
"fullName": "Иван Иванов",
"cardNumber": "2200700012345673"
},
"transactionProofUrl": "https://files.parallel-pay.com/payouts/cm3k8x7y80001z8j4k5m6n7o8/receipt-1.pdf",
"transactionProofUrls": [
"https://files.parallel-pay.com/payouts/cm3k8x7y80001z8j4k5m6n7o8/receipt-1.pdf"
],
"internalId": "payout-67890",
"merchantName": "MerchantName"
}Для событий спора
Тело — объект спора с новым статусом:
{
"id": "disp_abc123xyz789",
"invoiceId": "cm3k8x7y80001z8j4k5m6n7o8",
"reason": "invalid_sum",
"description": "Клиент отправил 500 RUB вместо 1000 RUB",
"disputeReasonData": {
"amount": 500
},
"attachmentUrl": "https://files.parallel-pay.com/disp_abc123xyz789/screenshot.jpg",
"attachmentFilename": "screenshot.jpg",
"status": "open",
"resolution": null,
"resolutionNotes": null,
"createdAt": "2025-11-03T15:10:00+03:00",
"resolvedAt": null,
"autoResolveAt": null,
"amount": "1000.0000",
"currency": "RUB",
"paymentMethod": "SBP",
"paymentOption": "sberbank",
"requisites": {
"bankName": "sberbank",
"phoneNumber": "79099966512",
"fullName": "Ivan Ivanov"
},
"internalId": "order-12345",
"merchantName": "MerchantName"
}Верификация подписи
Алгоритм подписи
Parallel подписывает каждый webhook запрос, используя HMAC-SHA256, для обеспечения подлинности и целостности данных.
Параметры алгоритма:
- Алгоритм: HMAC-SHA256
- Секретный ключ: ваш
notificationToken(32-255 символов) - Входные данные: JSON тело запроса (без модификаций)
- Выходной формат: Hex-кодированная строка
- Заголовок:
X-Webhook-Signature
Критически важно: используйте точное JSON тело запроса для верификации, без изменений форматирования, пробелов или порядка ключей. Любое изменение приведет к несовпадению подписи.
Примеры кода
Тип события рекомендуется определять по полю status в теле запроса, а не по заголовку X-Webhook-Event.
const crypto = require('crypto');
const express = require('express');
const app = express();
// ВАЖНО: Используйте raw body parser для webhook endpoint
app.use('/webhooks/parallel', express.json({
verify: (req, res, buf) => {
// Сохраняем raw body для верификации подписи
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhooks/parallel', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const event = req.headers['x-webhook-event'];
// Ваш notificationToken , переданный при создании заявки
const notificationToken = process.env.PARALLEL_NOTIFICATION_TOKEN;
// Проверка подписи
if (!verifySignature(req.rawBody, signature, notificationToken)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Обработка события
const payload = req.body;
console.log(`Received ${event} for invoice ${payload.id}`);
// Тип события — по статусу в теле запроса
switch (payload.status) {
case 'paid':
handleInvoicePaid(payload);
break;
case 'expired':
handleInvoiceExpired(payload);
break;
// ... другие статусы
}
// Важно: всегда возвращайте 200 для успешной обработки
res.status(200).send('OK');
});
function verifySignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expectedSignature = hmac.digest('hex');
// Используйте timing-safe сравнение для защиты от timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
} catch (err) {
// Невалидный hex или разная длина
return false;
}
}
function handleInvoicePaid(invoice) {
console.log(`Invoice ${invoice.id} paid: ${invoice.amount} ${invoice.currency}`);
// Найдите заказ по invoice.internalId. Если он уже оплачен — это повторная доставка, ничего не делайте.
// Иначе отметьте заказ как оплаченный, отправьте товар и т.д.
}
function handleInvoiceExpired(invoice) {
console.log(`Invoice ${invoice.id} expired`);
// Отметьте заказ как истекший
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});