Webhooks
Verify signatures
Check X-LokiPay-Signature in Node, PHP, Python and Go.
Every webhook carries X-LokiPay-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(WEBHOOK_SECRET, t + "." + rawBody). Verify it against the raw request body (before JSON parsing), compare in constant time, and reject timestamps older than 5 minutes to stop replays. This is the same scheme LokiPay v2 used, so existing verifiers keep working.
import crypto from 'node:crypto';
export function verifyLokiPay(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(String(header || '').split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: keep the raw body for this route
app.post('/api/payments/lokipay/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyLokiPay(raw, req.get('X-LokiPay-Signature'), process.env.LOKIPAY_WEBHOOK_SECRET)) return res.sendStatus(401);
const event = JSON.parse(raw);
if (event.type === 'payment.verified') fulfilOrder(event.data.reference, event.data.receivedAmountPesewas); // idempotent!
res.sendStatus(200);
});Use the raw body
Re-serialising parsed JSON changes whitespace and key order and breaks the signature. Read the body as bytes/text first.