Webhooks
Charge status notifications without polling the API
SplitPay sends events about a merchant's charges to your URL, so you don't have to poll GET /v1/charges/{id}.
Set the URL in the admin panel: Merchants β Webhooks. That is also where you find the signing secret (shown
once), the test event and the delivery log with manual retry.
Events
type | When |
|---|---|
charge.created | The charge was created |
charge.succeeded | The money was charged |
charge.failed | The bank declined |
charge.reversed | The charge was reversed |
charge.split_completed / charge.split_failed | The split succeeded / failed |
charge.fiscal_registered / charge.fiscal_failed | The OFD receipt was issued / failed |
webhook.test | A test event from the admin panel |
Request
A POST to your URL with a JSON body:
{
"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 has the same format as the GET /v1/charges/{id} response. Headers:
SplitPay-Event-Id, SplitPay-Event-Type and SplitPay-Signature.
Verifying the signature
SplitPay-Signature: t=<unix time>,v1=<signature>, where the signature is HMAC-SHA256 of the string t.body with the
whsec_β¦ secret. Verify the raw body before parsing JSON and reject events older than 5 minutes.
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); // event older than 5 minutes
}
const expected = createHmac('sha256', process.env.SPLITPAY_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest('hex');
// constant-time comparison so timing never leaks the signature
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') {
// order event.data.object.externalId is paid and split
}
res.sendStatus(200);
});Response and retries
- Reply with any
2xxwithin 10 seconds; do slow work after responding. - Otherwise the delivery is retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h and 12 h, then marked as failed. You can retry it from the admin panel.
- The same event may arrive twice: use
idto deduplicate. - A merchant's events are sent in order, but after retries the order is not guaranteed:
rely on
createdAtand on the status indata.object. - The URL must use
httpsand be reachable from the internet.