Вебхуки
Уведомления о статусе списаний без опроса 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и доступен из интернета.