SplitPay

Вебхуки

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

SplitPay сам присылает события по списаниям мерчанта на ваш адрес, так что опрашивать GET /v1/charges/{id} не нужно. Адрес задаётся в админке: Мерчанты → Вебхуки. Там же - секрет подписи (показывается один раз), тестовое событие и журнал доставок с ручным повтором.

События

typeКогда
charge.createdСписание создано
charge.succeededДеньги списаны
charge.failedБанк отказал
charge.reversedСписание отменено
charge.split_completed / charge.split_failedРаспределение выполнено / не выполнено
charge.fiscal_registered / charge.fiscal_failedЧек ОФД пробит / не пробит
webhook.testТестовое событие из админки

Запрос

POST на ваш адрес с телом JSON:

{
  "id": "0f8d3c1e-…",
  "type": "charge.split_completed",
  "createdAt": "2026-09-27T10:00:00.000Z",
  "data": { "object": { "id": "…", "externalId": "order-777", "status": "SUCCEEDED", "split": { "status": "COMPLETED" } } }
}

data.object имеет тот же формат, что ответ GET /v1/charges/{id}. Заголовки: SplitPay-Event-Id, SplitPay-Event-Type и SplitPay-Signature.

Проверка подписи

SplitPay-Signature: t=<unix-время>,v1=<подпись>, где подпись это HMAC-SHA256 от строки t.тело на секрете whsec_…. Проверяйте сырое тело до разбора JSON и отклоняйте события старше 5 минут.

import { createHmac, timingSafeEqual } from 'node:crypto';

app.post('/hooks/splitpay', express.text({ type: 'application/json' }), (req, res) => {
  const header = req.header('splitpay-signature') ?? '';
  const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')));

  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) {
    return res.sendStatus(400); // событие старше 5 минут
  }

  const expected = createHmac('sha256', process.env.SPLITPAY_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`)
    .digest('hex');

  // сравнение за постоянное время, чтобы не подсказывать подпись по таймингу
  const ok = v1?.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!ok) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body);
  if (event.type === 'charge.split_completed') {
    // заказ event.data.object.externalId оплачен и распределён
  }
  res.sendStatus(200);
});

Ответ и повторы

  • Ответьте любым кодом 2xx в течение 10 секунд; долгую обработку делайте после ответа.
  • Иначе доставка повторяется через 1 мин, 5 мин, 15 мин, 1 ч, 3 ч, 6 ч и 12 ч, затем помечается ошибкой. Повторить её можно из админки.
  • Одно событие может прийти дважды: используйте id для защиты от повторов.
  • События одного мерчанта отправляются по порядку, но после повторов порядок не гарантирован: ориентируйтесь на createdAt и на статус в data.object.
  • Адрес должен быть https и доступен из интернета.

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