Skip to main content

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:

  1. Set your endpoint URL. It must be a public https URL. Private, loopback, and link-local addresses are rejected.
  2. 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).
  3. 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

EventWhen
transaction.successA purchase was delivered
transaction.failedA 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);
}
tip

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.