Webhooks
Get notified when a payment succeeds, fails, or expires, and verify it came from NuruPay.
NuruPay sends an HTTPS POST to your webhook endpoint when a collection reaches a final state. During the pilot, NuruPay registers your endpoint and gives you its signing secret (whsec_…, shown once).
Events
| Event | When |
|---|---|
collection.succeeded | The customer paid. |
collection.failed | Declined, insufficient funds, cancelled, or rejected; see data.failure_code. |
collection.expired | Not approved in time; the order was cancelled. |
Payload
{
"id": "evt_01M3P5FBS9V5FJ1P5R0N8Z64XX",
"type": "collection.succeeded",
"created_at": "2026-09-29T08:46:05Z",
"mode": "live",
"data": { "id": "col_…", "object": "collection", "status": "succeeded", "amount": 1000, "...": "..." }
}data is the full collection object at the moment of the event.
Verify the signature
Every webhook carries a NuruPay-Signature header:
NuruPay-Signature: t=1759132800,v1=c57e6ec5e76a0cdd68ee3fd1eba420ff9abf04cd9755d174d59e1c4d6e926a54v1 is the hex HMAC-SHA256 of "<t>.<raw request body>" using your signing secret. Always verify it before trusting the body, and reject timestamps older than five minutes to stop replays.
Verify against the raw bytes you received. Parsing the JSON and serialising it again changes the bytes and breaks the signature.
import crypto from 'node:crypto';
// Returns true only if the webhook is genuine and fresh.
// rawBody must be the exact bytes received (do not re-serialise JSON).
export function verifyNuruPaySignature(rawBody, header, secret, toleranceSeconds = 300, now = Date.now()) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=', 2)));
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
if (Math.abs(now / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Respond quickly
- Return any
2xxwithin 10 seconds, then do slow work (sending emails, updating stock) in the background. - Anything else, including timeouts and redirects, counts as a failure. NuruPay retries after 1m, 5m, 30m, 2h, 6h, and 12h, then marks the delivery failed.
- An endpoint that fails continuously for three days is disabled; NuruPay re-enables it once you have fixed it.
Handle duplicates and ordering
- Delivery is at least once: you may receive the same event twice. Store the event
idand ignore repeats. - Events can arrive out of order. If order matters, fetch the collection with
GET /v1/collections/{id}for its current state. - To double-check an event, fetch it with
GET /v1/events/{id}using your API key.
Endpoint requirements
- Live-mode endpoints must use
https://. - Endpoints must be publicly reachable; private and internal addresses are refused.