Preview: the API is being built and these endpoints may change

Webhooks

Events, signatures and retries.

Add an endpoint under Developers > Webhooks. We send a POST with a JSON body for each event.

Events

EventWhen
transaction.successfulA sale delivered and its fee was settled
transaction.failedA sale failed and its hold was released
transaction.pendingA sale is waiting on the network's answer
transaction.refundedWe refunded a sale
wallet.fundedA top-up was credited
wallet.low_balanceThe wallet dropped below your alert level
webhook.pingA test you sent from the dashboard

The body

json
{
  "id": "0192f4c2-8d1e-7a3b-9c4f-5e6a7b8c9d0e",
  "type": "transaction.successful",
  "mode": "live",
  "occurred_at": "2026-10-06T12:04:03Z",
  "data": {
    "reference": "txn_0192f4c1a7e84b6f9d2c3e5a7b9d1f30",
    "client_reference": "order_1042",
    "status": "successful",
    "product_code": "MTN-VTU",
    "network": "mtn",
    "service": "airtime",
    "destination": "+234********78",
    "amount": 500.00,
    "price": 2.00,
    "completed_at": "2026-10-06T12:04:03Z"
  }
}

A failed sale adds failure_code. wallet.funded carries funding_id, amount, balance and gateway; wallet.low_balance carries balance and threshold. Amounts are naira, numbers with two decimals.

Checking the signature

Every request carries a SimConnect-Signature header: t=<unix time>,v1=<signature>. The signature is HMAC SHA-256 of <t>.<raw body> with your endpoint secret, in hex.

js
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Refuse anything older than five minutes, so a captured request cannot be replayed.

Retries

Answer with any 2xx within ten seconds to confirm. Anything else, or no answer, is retried from 30 seconds later, doubling each time, for up to 8 attempts. Each event has an id, also sent as the SimConnect-Event-Id header, so handle it once even if it arrives twice. Failed deliveries can be replayed from the dashboard.