API Reference
Webhooks
Infinity Africa notifies your server the moment a collection resolves, a payout completes, or an invoice gets paid — so you don't have to poll. Configure your endpoint once; every event after that is pushed to you.
Configuring your endpoint
Set your webhook URL and choose which events to subscribe to from the Merchant Portal's Webhooks page — generate a signing secret there too (shown once, like an API key), and use the page's Send Test Webhook button to confirm your endpoint is reachable before going live. The same page shows the status of your most recent delivery attempt.
/v1/merchant/webhook-configRead your current webhook URL, subscribed events, and whether a secret is set.
MERCHANT_ADMIN, MERCHANT_STAFF/v1/merchant/webhook-configSet the URL/events, and optionally regenerate the signing secret (returned once, in the response).
MERCHANT_ADMIN, DEVELOPER/v1/merchant/webhook-config/testSend a signed sample payload to your configured URL right now and report the result.
MERCHANT_ADMIN, DEVELOPER/v1/merchant/webhook-eventsList your own outbound delivery queue.
MERCHANT_ADMIN, MERCHANT_STAFFEvent types
The events below are the ones your integration should actually handle. A few additional names are reserved in the schema for features on the roadmap (refunds, chargebacks, scheduled sweeps) — you'll see them in the enum, but nothing emits them yet.
| Event | Fires when |
|---|---|
| collection.success | A collection reached successful — the merchant wallet was credited. The only event safe to mark an order paid from. |
| collection.failed | A push or QR collection was declined, rejected, or timed out. |
| collection.pending_review | Held before crediting — the payer's phone matched the merchant's own registered phone (self-payment/"own till" risk). Requires Super Admin review. |
| collection.reversed | A previously successful collection was reversed by the provider after settlement — the wallet credit was clawed back. |
| disbursement.success | A payout was delivered. |
| disbursement.failed | A payout was declined; its balance reservation was reversed. |
| disbursement.reversed | A previously successful payout was reversed by the provider after settlement. |
| payment_link.paid | A payment link (including one generated from an invoice) was paid. |
| payment_link.payment_reversed | A payment link's PAID status was reopened because its collection was reversed. |
| invoice.paid | An invoice reached PAID. |
| collection.pending | Reserved for a future intermediate collection state.(reserved) |
| collection.processing | Reserved — a push was sent or a QR/token was generated; not emitted as its own event yet (visible via GET/refresh-status instead).(reserved) |
| collection.cancelled | Reserved — no code path sets a collection to cancelled yet.(reserved) |
| invoice.overdue | Reserved for a scheduled past-due sweep.(reserved) |
| payment_link.created | Reserved.(reserved) |
| payment_link.expired | Reserved for a scheduled expiry sweep.(reserved) |
| refund.succeeded | Reserved — refunds aren't issued yet.(reserved) |
| refund.failed | Reserved.(reserved) |
| chargeback.opened | Reserved.(reserved) |
| chargeback.resolved | Reserved.(reserved) |
Status lifecycle
The status field on a collection webhook (and on GET /v1/collections/{id}) is always one of these seven values:
| Status | Meaning |
|---|---|
| created | Collection created (an Infinity Payment Page with no method chosen yet). Payment has not started. |
| processing | A prompt was sent or a QR/token was generated. The customer has not yet approved anything. |
| pending_clearance | The provider signaled completion, but a review step still applies before funds become available (currently: self-payment/"own till" risk review). Not yet safe to treat as paid. |
| successful | Final, safe, completed state. The merchant wallet was credited. The only status safe to mark an order paid from. |
| failed | The attempt did not complete. Not payable, not credited. |
| cancelled | The attempt was cancelled. Not payable, not credited. |
| reversed | Was successful, then clawed back by the provider after settlement. Not credited — treat exactly like a failed payment. |
Mark an order paid only on collection.successful
000 on the initial push response — all of those only mean the provider accepted the request, not that the customer paid. Wait for collection.successful (webhook) or poll GET /v1/collections/{id} until status is successful.Payload shape
{
"event": "collection.successful",
"merchant_code": "27048391",
"collection_id": "col_xxxxx",
"transaction_id": "txn_xxxxx",
"reference": "ORDER-4821",
"merchant_reference": "ORDER-4821",
"amount": 50000,
"fee": 750,
"net_amount": 49250,
"currency": "TZS",
"status": "successful",
"timestamp": "2026-08-23T10:00:00+03:00"
}merchant_code is your Merchant ID — identification only, never a secret and never usable as an API key. reference and merchant_reference are the same value — kept as two keys so existing integrations parsing reference keep working.
fee/net_amount are only present once a fee has actually been calculated (i.e. from collection.successful/collection.reversed onward) — never fabricated for an event where no fee exists yet.
Verifying a delivery
Every delivery is signed with your merchant's webhook secret, sent as X-Infinity-Signature: an HMAC-SHA256 hex digest of the exact raw request body. Recompute it and compare — don't trust a delivery that doesn't match, and use a constant-time comparison to avoid leaking timing information.
import hashlib
import hmac
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)const crypto = require("crypto");
function verifySignature(rawBody, signature, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signature || "", "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example — read the raw body BEFORE any JSON-parsing middleware
// runs, since the signature is computed over the exact raw bytes:
app.post(
"/api/infinity/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("X-Infinity-Signature");
if (!verifySignature(req.body, signature, process.env.INFINITY_WEBHOOK_SECRET)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body);
// ... handle event.event / event.status / event.collection_id
res.status(200).send("ok");
},
);Retries
Every event is recorded to your delivery queue the moment it happens. Automatic retry-with-backoff delivery is on the roadmap but not live yet — for now, use Send Test Webhook on the Portal's Webhooks page to confirm your endpoint responds correctly, and Transaction Status to poll as a fallback. Respond quickly to real deliveries once retries ship — do your processing asynchronously after returning 200, rather than making Infinity Africa wait on it.
Design for at-least-once delivery
payload (e.g. collection_id) and make handling that ID idempotent now, so a duplicate delivery is a safe no-op later.