Skip to main content

Webhooks

Receive real-time notifications when events occur — transactions completing, accounts opening, loans being approved, and more.

Setup​

Configure webhooks in two ways:

  1. Dashboard — Log in to the dashboard, navigate to Settings → Webhooks, and add your endpoint URL.
  2. API — Register a webhook subscription programmatically:
{
"name": "My Webhook",
"url": "https://example.com/webhooks/cba",
"secret": "whsec_your_secret_here",
"events": ["transaction.completed", "account.opened", "transfer.failed"],
"is_active": true
}

The secret is used to compute HMAC signatures. Store it securely.

Payload Format​

Every webhook delivery is an HTTP POST with a JSON body:

{
"id": "evt-a1b2c3d4",
"type": "transaction.completed",
"tenant_id": "tenant-uuid",
"data": {
"transaction_id": "txn-uuid",
"transaction_ref": "TRN20260228001234",
"amount": 100000.00,
"currency": "NGN",
"status": "COMPLETED"
},
"created_at": "2026-02-28T10:00:00Z"
}

Headers​

HeaderDescription
Content-Typeapplication/json
X-Webhook-Signaturet=<unix_timestamp>,v1=<hmac_hex>
X-Webhook-TimestampUnix timestamp of the signature
X-Webhook-IDUnique delivery ID for idempotency

Signature Verification​

The signature uses HMAC-SHA256. The signed payload is {timestamp}.{raw_body}.

const crypto = require("crypto");

function verifyWebhookSignature(body, signatureHeader, secret) {
const [tPart, vPart] = signatureHeader.split(",");
const timestamp = tPart.split("=")[1];
const signature = vPart.split("=")[1];

// Check timestamp tolerance (5 minutes)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
throw new Error("Timestamp outside tolerance");
}

const signedPayload = `${timestamp}.${body}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

// Express middleware
app.post("/webhooks/cba", (req, res) => {
const sig = req.headers["x-webhook-signature"];
if (!verifyWebhookSignature(JSON.stringify(req.body), sig, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
handleEvent(req.body);
res.status(200).json({ received: true });
});
warning

Always use constant-time comparison to prevent timing attacks. Validate the timestamp to reject replayed webhooks.

Event Types​

Account Events​

EventDescription
account.openedNew account created
account.closedAccount closed
account.frozenAccount frozen
account.unfrozenAccount unfrozen

Transaction Events​

EventDescription
transaction.createdTransaction initiated
transaction.completedTransaction successful
transaction.failedTransaction failed
transaction.reversedTransaction reversed

Transfer Events​

EventDescription
transfer.initiatedTransfer started
transfer.completedTransfer delivered
transfer.failedTransfer failed

Customer Events​

EventDescription
customer.createdNew customer registered
customer.updatedCustomer profile updated
customer.kyc.approvedKYC verification approved
customer.kyc.rejectedKYC verification rejected

Loan Events​

EventDescription
loan.appliedLoan application submitted
loan.approvedLoan approved
loan.disbursedLoan funds disbursed
loan.repaidLoan fully repaid
loan.defaultedLoan in default

Security Events​

EventDescription
security.login.successSuccessful login
security.login.failedFailed login attempt
security.mfa.enabledMFA enabled on account
security.password.resetPassword reset completed

Retry Behavior​

Failed deliveries (non-2xx response) are retried with exponential backoff:

AttemptDelay
11 second
22 seconds
34 seconds
41 minute
55 minutes

After 5 failed attempts the delivery is marked as failed. Timeout per delivery is 30 seconds.

Best Practices​

  • Return 200 quickly — Acknowledge receipt, then process asynchronously.
  • Handle duplicates — Use X-Webhook-ID for idempotency.
  • Verify signatures — Always validate before processing.
  • Validate timestamps — Reject events older than 5 minutes.
  • Use HTTPS — Webhook URLs must be served over TLS.

Next Steps​