Webhooks
Purchases that settle asynchronously (status Processing) resolve in the background. Rather than poll, point us at a URL and we'll POST you the terminal result.
Set your webhook URL (and you'll be given a signing secret) from your dashboard.
Events
| Event | When |
|---|---|
transaction.success | A purchase was delivered |
transaction.failed | A purchase failed and was refunded to your wallet |
Payload
We POST JSON like:
{
"event": "transaction.success",
"reference": "PLS-9KQ2M7P…",
"clientReference": "order-1001",
"status": "Success",
"service": "Data",
"amount": 27000,
"mobileNumber": "08030000000",
"providerReference": "118682181",
"completedAt": "2026-06-12T10:14:05Z"
}
amount is in kobo. Match reference — or your own clientReference — to the original purchase. clientReference is the idempotency key you sent, so you can correlate the event to your order even if the original purchase response never reached you.
Verifying the signature
Every delivery carries an HMAC signature so you can be sure it came from us:
X-Plustive-Signature: sha256=<hex>
It's HMAC-SHA256 of the raw request body, keyed by your webhook secret. Recompute it and compare in constant time:
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verify against the raw body bytes, before any JSON parsing/re-serialization — reformatting changes the signature.
Responding
Return any 2xx to acknowledge. Non-2xx (or a timeout) is retried with backoff, so make your handler idempotent — key on reference.