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.
Configure your webhook
Set everything up from Portal → Webhooks:
- Set your endpoint URL. It must be a public
httpsURL. Private, loopback, and link-local addresses are rejected. - Generate signing secret. Click Generate signing secret and copy it — it's shown once, so store it somewhere safe. You'll use it to verify the signature on every delivery (see Verifying the signature).
- Send a test event. Click Send test event to confirm your endpoint receives the request and returns a
2xx.
A few things to know:
- Regenerating the secret takes effect immediately — the next delivery is signed with the new secret, so roll it out to your handler at the same time.
- Webhooks fire only on terminal states (
transaction.success/transaction.failed), never while a purchase is still pending.
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", // Data | Airtime | CableTv | Electricity | Education | Broadband
"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.
service is one of: Data, Airtime, CableTv, Electricity, Education, Broadband.
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.