Webhooks
MagiaPay POSTs signed JSON event envelopes to your registered endpoints. Every delivery carries an HMAC-SHA256 signature; verify it before trusting the payload.
Headers delivered
| Header | Example | Meaning |
|---|---|---|
X-MagiaPay-Signature | t=1776840000,v1=a3f8… | HMAC-SHA256, see below. |
X-MagiaPay-Event | payment.succeeded | Event type for routing. |
X-MagiaPay-Delivery | 42 | Numeric delivery attempt id. |
User-Agent | MagiaPay/1.0 | Lets you allowlist traffic. |
Signature format
HMAC-SHA256 format
X-MagiaPay-Signature: t=<unix>,v1=<hex>
signedPayload = `${t}.${rawBody}`
signature = hmacSha256(signingSecret, signedPayload)Three gotchas:
- Compute HMAC over the raw request body. Don't JSON-parse-then-re-serialise — whitespace differences will break the hash.
- Reject any webhook where
|now − t| > 300 secondsto prevent replay attacks. - Use constant-time equality (
crypto.timingSafeEqual) to compare signatures.
Verify the signature
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) 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);
}Event envelope
POST /your-webhook
{
"id": "evt_2e599b29fc3203f06a63cc16",
"object": "event",
"type": "payment.succeeded",
"environment": "live",
"created": 1776840100,
"data": {
"object": {
"id": "pay_0e1f2a4c9b8d6a3f5c7e1d2b",
"object": "payment",
"status": "succeeded",
"amount": 500.00,
"currency": "PHP",
"method": "gcash_qr",
"provider": "directpay",
"fee_amount": 12.50,
"net_amount": 487.50,
"created": 1776839700
}
}
}Retry schedule
Non-2xx responses trigger retries at 1m, 5m, 30m, 2h, 6h, 24h. After 6 failures the delivery is parked as failed. Replay manually from the dashboard (/dashboard/webhooks) or via GET /v1/events reconciliation.
Respond fast
Your handler should acknowledge in under 10 seconds. If heavy processing is needed, enqueue the payload to a background worker and return 200 immediately — otherwise the delivery will time out and retry.