Guides
Webhooks
Payjar tells your server when a transfer arrives, even if the customer closed the page.
A webhook is how Payjar tells your server that a transfer arrived, even if the customer closed the page.
Set the URL in the Payjar dashboard, once for test and once for live. When you save it, Payjar shows a signing secret that starts with whsec_. Store that secret. Payjar uses it to sign every webhook. Register first if you do not have a dashboard yet.
Payjar sends one event today:
| Event | When |
|---|---|
payment.success | The payment has just been marked success. |
Request to your server#
POST /webhooks/payjar HTTP/1.1
Host: example.com
Content-Type: application/json
X-Supapay-Event: payment.success
X-Supapay-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
{
"event": "payment.success",
"data": {
"id": 1042,
"reference": "order_1001",
"access_code": "a1b2c3d4e5f6789012345678abcdef01",
"amount": 500000,
"fee": 5000,
"charged_amount": 500000,
"settled_amount": 495000,
"fee_paid_by": "merchant",
"currency": "NGN",
"status": "success",
"environment": "test",
"customer_id": 88,
"customer_email": "[email protected]",
"customer_name": "Ada Lovelace",
"customer_phone": "08031234567",
"metadata": { "order_id": "1001" },
"bank": {
"bank_name": "9JAPAY MFB",
"account_name": "SUPAPAY CHECKOUT",
"account_number": "0123456789",
"amount": 500000,
"currency": "NGN"
},
"paid_at": "2026-11-03T16:41:12+01:00",
"expires_at": "2026-11-03T17:07:00+01:00",
"created_at": "2026-11-03T16:37:00+01:00"
}
}X-Supapay-Signature is the hex HMAC-SHA512 of the raw request body, using your webhook secret. Read the body as bytes first. If you parse the JSON and encode it again, the signature will not match.
Reply with a 2xx status as soon as you have accepted the event. Do the slow work after that. Payjar waits up to 8 seconds.
If your server does not reply with 2xx, Payjar tries again. It tries up to 5 times. The wait after each failed try doubles: about 2 minutes, then 4, then 8, then 16. After 5 failed tries, Payjar stops.
The same payment can be sent more than once. Use data.reference so you deliver the order one time.
Check the signature#
import crypto from 'node:crypto';
function isPayjarWebhook(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac('sha512', secret).update(rawBody).digest('hex');
const given = Buffer.from(signatureHeader || '', 'utf8');
const actual = Buffer.from(expected, 'utf8');
return given.length === actual.length && crypto.timingSafeEqual(given, actual);
}
// Express: use the raw body, not the parsed object.
app.post('/webhooks/payjar', express.raw({ type: 'application/json' }), (req, res) => {
const ok = isPayjarWebhook(
req.body,
req.get('X-Supapay-Signature'),
process.env.PAYJAR_WEBHOOK_SECRET
);
if (!ok) {
res.status(400).send('Invalid signature');
return;
}
const event = JSON.parse(req.body.toString('utf8'));
if (event.event === 'payment.success' && event.data.status === 'success') {
// Mark order event.data.reference as paid.
}
res.sendStatus(200);
});A bad signature should get an HTTP 400 from you. Payjar will retry 4xx and 5xx responses, so fix the secret or the raw-body handling rather than returning 400 forever for a real event you already understand.
