Parallel

Webhook-уведомления

Уведомления об изменении статуса заявок и споров

Обзор

Система webhook-уведомлений Parallel обеспечивает надежную доставку событий об изменении статуса заявок на ваш endpoint.

События webhook

Типы событий

Webhook отправляются при следующих изменениях статуса заявки:

ТриггерКогда отправляется
Заявка оплаченаПосле успешного подтверждения платежа и зачисления средств
Истек срокПосле истечения 15 минут без оплаты
ОтмененаПри ручной отмене мерчантом или администратором
Создан спорОткрыт спор по заявке
Спор решенСпор закрыт с решением

Формат 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-SignatureHMAC-SHA256 подпись тела запроса в hex-формате. Используйте для проверки подлинности webhook (см. Верификация подписи)
X-Webhook-EventТип события (например, invoice.paid, invoice.expired). Позволяет быстро определить тип события без парсинга тела, но для безопасности обязательно сверяйте его со 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",
  "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\":\"sberbank\",\"fullName\":\"Иван Иванов\",\"phoneNumber\":\"79001234567\"}",
  "internalId": "order-12345",
  "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": "2025-11-03T16:10:00+03:00",
  "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');
});

На этой странице