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