Webhooks
Payder calls your server when a payment, refund or payout reaches its final state. Webhooks are the source of truth for money events.
Set your endpoint
In the console, open Webhooks, enter your URL (for example https://api.yourshop.com/api/webhooks/payder) and save. Click Generate secret to create the signing secret. It is shown once; use Rotate to replace it. In live, the URL must be https and publicly reachable.
The request
Payder sends a POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
x-payder-signature | Lowercase hex HMAC-SHA256 of the raw request body, keyed with your webhook secret |
x-payder-timestamp | Unix seconds when this attempt was sent |
x-payder-signature-v2 | Lowercase hex HMAC-SHA256 of "<timestamp>.<raw body>" — use this one if you want to reject old replays by checking the timestamp |
{
"id": "evt_8f3k2m1x",
"event": "payment.succeeded",
"reference": "ORD-1001",
"amountMinor": 1250000,
"currency": "NGN",
"occurredAt": "2026-10-04T18:02:11.000Z"
}id is unique per event, so you can ignore one you have already handled. reference is your reference for the checkout, refund or payout, so you can find the record in your own database.
Events
| Event | Sent when |
|---|---|
payment.succeeded | A checkout was paid. |
payment.failed | A checkout failed or expired. |
refund.succeeded | A refund was completed. |
refund.failed | A refund could not be completed. |
payout.succeeded | A payout reached the seller's account. |
payout.failed | A payout failed. Its amount is back in your balance. |
escrow.funded | A buyer paid an escrow. See Escrow for the other escrow.* events. |
Verify the signature
Always verify before you act. Compute the HMAC over the raw bytes of the body, not over re-serialised JSON, and compare in constant time.
import crypto from 'crypto';
import express from 'express';
const app = express();
// Use the RAW body: the signature covers the exact bytes Payder sent.
app.post('/api/webhooks/payder', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.PAYDER_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const given = String(req.headers['x-payder-signature'] || '');
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const evt = JSON.parse(req.body.toString());
// De-duplicate on evt.event + evt.reference, then update your order.
res.sendStatus(200);
});Common mistake
Acknowledge, retries and duplicates
- Reply with any 2xx status as soon as you have stored the event. Do slow work afterwards.
- Anything else, or a timeout, is retried: after 1 minute, 5 minutes, 30 minutes, 2 hours, then every 12 hours, until 24 hours after the event. After that the delivery is marked abandoned.
- The same event can arrive more than once, and out of order. Make your handler idempotent by keying on
eventplusreference. - If you missed events while your server was down, use
GET /v1/checkouts/:reference(or refunds, payouts) to reconcile, or press Resend in the console.
Delivery log and resend
The console lists every delivery with its attempts, your server's last HTTP status, any error, and the exact payload that was sent. Press Resend to send any event again; it appears as a new delivery.
Checklist
- Verified signature on the raw body
- Checks
amountMinorandcurrencyagainst the order - Idempotent on
event+reference - Returns 2xx quickly, and 401 for a bad signature
- Webhook secret stored in a secret manager, never in code