SplitPay

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

typeWhen
charge.createdThe charge was created
charge.succeededThe money was charged
charge.failedThe bank declined
charge.reversedThe charge was reversed
charge.split_completed / charge.split_failedThe split succeeded / failed
charge.fiscal_registered / charge.fiscal_failedThe OFD receipt was issued / failed
webhook.testA 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 2xx within 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 id to deduplicate.
  • A merchant's events are sent in order, but after retries the order is not guaranteed: rely on createdAt and on the status in data.object.
  • The URL must use https and be reachable from the internet.

On this page