Webhooks
Receive real-time notifications when events occur in the banking platform — transfers completing, loan approvals, fraud alerts, and more.
Setup
- Log in to the dashboard and navigate to Settings → Webhooks.
- Add your webhook URL and select the event types you want to receive.
- Save the webhook secret — it's used for signature verification and is only shown once.
You can also register webhooks programmatically via the API.
Payload Format
Every webhook delivery is an HTTP POST with a JSON body:
{
"id": "evt_a1b2c3d4",
"eventType": "transaction_completed",
"tenantId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-27T14:22:00Z",
"data": {
"transactionId": "t9a8b7c6-...",
"reference": "FMFB20260227001234",
"amount": 100000.00,
"status": "completed",
"fromAccount": "0000123456",
"toAccount": "0000654321"
}
}
Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC-SHA256 signature of the request body |
X-Webhook-Event | Event type (e.g. transaction_completed) |
X-Webhook-ID | Unique delivery ID for idempotency |
X-Webhook-Timestamp | ISO 8601 timestamp |
Signature Verification
Verify the X-Webhook-Signature header to confirm the payload came from the Banking API and wasn't tampered with.
The signature is computed as HMAC-SHA256(webhook_secret, raw_request_body) and sent as a hex string.
- Node.js
- Python
- Go
const crypto = require("crypto");
function verifyWebhookSignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express middleware
app.post("/webhooks/banking", (req, res) => {
const signature = req.headers["x-webhook-signature"];
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
// Process the event
handleEvent(req.body);
res.status(200).json({ received: true });
});
import hmac
import hashlib
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route("/webhooks/banking", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Webhook-Signature")
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
handle_event(request.json)
return jsonify({"received": True}), 200
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifyWebhookSignature(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
Always use constant-time comparison (timingSafeEqual, hmac.compare_digest, hmac.Equal) to prevent timing attacks.
Event Types
| Event | Description | Trigger |
|---|---|---|
transaction_completed | Transfer or payment succeeded | Transfer settles |
transaction_failed | Transfer or payment failed | Transfer rejected by NIBSS/provider |
loan_application | Loan application submitted | User applies for a loan |
loan_approved | Loan application approved | Final approval level passed |
loan_disbursed | Loan funds disbursed | Amount credited to wallet |
security_alert | Security event detected | Password change, new device login |
fraud_alert | Potential fraud flagged | Risk score exceeds threshold |
kyc_required | KYC action needed | User reaches a limit requiring higher KYC |
limit_exceeded | Transaction limit breached | Daily/monthly limit hit |
account_update | Account status changed | Wallet frozen, KYC level upgraded |
payment_reminder | Upcoming payment due | Loan repayment or bill due soon |
ai_insight | AI-generated insight | Spending anomaly detected |
Example Payloads
transaction_completed
{
"id": "evt_tx_001",
"eventType": "transaction_completed",
"tenantId": "550e8400-...",
"timestamp": "2026-02-27T14:22:00Z",
"data": {
"transactionId": "t9a8b7c6-...",
"type": "external_transfer",
"reference": "FMFB20260227001234",
"amount": 100000.00,
"fees": 100.00,
"status": "completed",
"fromAccount": "0000123456",
"toAccount": "0000654321",
"toBank": "Access Bank"
}
}
fraud_alert
{
"id": "evt_fraud_001",
"eventType": "fraud_alert",
"tenantId": "550e8400-...",
"timestamp": "2026-02-27T14:22:00Z",
"data": {
"userId": "b47ac10b-...",
"riskScore": 85,
"riskLevel": "critical",
"decision": "block",
"flags": ["unusual_amount", "new_device", "foreign_ip"],
"transactionId": "t9a8b7c6-..."
}
}
loan_approved
{
"id": "evt_loan_001",
"eventType": "loan_approved",
"tenantId": "550e8400-...",
"timestamp": "2026-02-27T14:22:00Z",
"data": {
"applicationId": "d4c3b2a1-...",
"applicationNumber": "LN20260227001",
"approvedAmount": 500000.00,
"interestRate": 18.5,
"tenureMonths": 12,
"approvalLevel": "branch_manager"
}
}
Retry Behavior
Failed deliveries (non-2xx response) are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
After 5 failed attempts, the webhook is marked as failed. Use the X-Webhook-ID header for idempotency — you may receive the same event more than once.
Best Practices
- Return 200 quickly — Process events asynchronously. Acknowledge receipt immediately, then handle the event in a background job.
- Handle duplicates — Use
X-Webhook-IDto deduplicate. Store processed event IDs and skip duplicates. - Verify signatures — Always validate the HMAC signature before processing any event.
- Monitor failures — Set up alerts for webhook delivery failures and investigate promptly.
- Use HTTPS — Your webhook endpoint must be served over HTTPS.
Next Steps
- Server Integration — Backend setup and error handling.
- Transfers — Transfer events and status tracking.
- Error Codes — Error code reference.