Webhooks
Webhooks let your systems react to MFI events in real time instead of polling. Configure subscribers per event type with retry policies and verify signatures with a shared secret.

At a glance — Transport: HTTPS POST. Format: JSON envelope with event + payload. Signature: X-MFI-Signature header HMAC-SHA256 of body using subscriber secret. Retry: 3 attempts with backoff 1m, 5m, 30m. Dead letter queue after 3 failures.
Event catalogue
| Event | Fires when | Payload |
|---|---|---|
| member.created | New member record committed | Member object |
| member.kyc_completed | KYC verification approved | Member + KYC bundle |
| loan.created | Loan created in draft | Loan object |
| loan.approved | Loan state moves to approved | Loan object |
| loan.disbursed | Disbursement confirmed by channel | Loan + disbursement object |
| loan.repaid | Repayment posted | Loan + repayment + new balance |
| loan.delinquent | Loan enters PAR>30 | Loan + days past due |
| loan.written_off | Loan written off | Loan object |
| savings.deposit_posted | Deposit booked | Account + transaction |
| savings.withdrawal_posted | Withdrawal booked | Account + transaction |
| aml.alert_raised | New AML alert | Alert + triggering transactions |
| regulatory.return_lodged | Return submitted to supervisor | Return object |
Event envelope
{
"id": "evt_01HXY9TZQ7K2P8",
"event": "loan.disbursed",
"created": "2026-03-15T10:42:17.182Z",
"tenant": "microfinance.mybridgeerp.com",
"api_version": "v1",
"data": {
"loan": {
"id": 9183,
"loan_no": "L-2026-09183",
"member_id": 4827,
"member_name": "Jane Wambui Mwangi",
"principal": 5000000,
"state": "active"
},
"disbursement": {
"id": 7211,
"amount": 5000000,
"channel": "mpesa_b2c",
"provider_ref": "PBJ4S6X9DC",
"posted_at": "2026-03-15T10:42:15Z"
}
}
}
Verifying the signature
Every webhook POST carries an X-MFI-Signature header containing an HMAC-SHA256 hex digest of the raw request body, computed with the subscriber secret. Always verify before processing.
import hmac, hashlib
def verify(secret: bytes, body: bytes, signature: str) -> bool:
expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
@app.post('/mfi-webhook')
def receive():
sig = request.headers.get('X-MFI-Signature', '')
if not verify(SECRET, request.data, sig):
abort(401)
event = request.json
process(event)
return '', 200
Delivery and retry
- First attempt fires immediately after the source action commits
- If subscriber returns non-2xx or times out (default 10s), event queued for retry
- Retry attempt 1 after 1 minute
- Retry attempt 2 after 5 minutes
- Retry attempt 3 after 30 minutes
- After 3 failed retries, event moves to dead letter queue in Settings > API > Webhooks > Failed
Warning — Webhooks may arrive out of order. Always idempotent-process by event id; deduplicate on (event id, event type).
Configuring a subscriber
Settings > Technical > Webhooks > Create.
- Name — human label
- URL — HTTPS endpoint
- Secret — generated once, never shown again
- Events — multi-select event types to subscribe to
- Active — toggle to disable without deleting
- Headers — optional custom headers (e.g. Authorization)
Worked scenarios
Scenario — SMS reminders via webhook
| Character | Role |
|---|---|
| Mary Mutua | Customer Communications Lead |
| Samuel Kibet | DevOps Engineer |
| Jane Wambui | Borrower |
Timeline
- Day 1, 10:00: Samuel creates webhook subscriber. (URL https://sms-bridge.mwananchi.co.ke/inbound, events loan.delinquent.)
- Day 1, 11:00: Samuel deploys SMS bridge service. (Bridge calls Africa's Talking SMS API on each event.)
- Day 31, 09:00: Engine detects Jane's loan at 31 DPD. (loan.delinquent event fires.)
- Day 31, 09:00:02: SMS bridge receives webhook, verifies HMAC. (Signature ok; bridge invokes Africa's Talking.)
- Day 31, 09:00:05: Jane's phone vibrates. ('Dear Jane, your loan L-2026-09183 is 31 days overdue. Please pay KES 18,540 today to avoid CRB listing. Mwananchi SACCO.')
- Day 31, 14:00: Jane pays via M-Pesa Paybill. (Repayment posted; loan.repaid event fires.)
- Day 31, 14:00:02: SMS bridge sends thank-you. ('Thank you Jane. KES 18,540 received. Your loan is now current.')
Outcome — SMS reminders fully automated without polling. Mary tracks effectiveness via the webhook delivery dashboard and SMS delivery reports.
Reference
Webhook request headers
| Header | Value | Purpose |
|---|---|---|
| Content-Type | application/json | Always JSON |
| X-MFI-Event | Convenience copy of envelope event | |
| X-MFI-Event-Id | Convenience copy of envelope id | |
| X-MFI-Signature | HMAC-SHA256 of body | |
| X-MFI-Delivery-Attempt | 1, 2, 3, or 4 | Retry counter (4 = dead letter) |
| X-MFI-Tenant | Source tenant | |
| User-Agent | MFI-Webhooks/1.0 | Identification |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| All webhooks failing with 401 from subscriber. | Subscriber's secret rotated but MFI not updated. | Settings > Webhooks > Edit > regenerate or paste new secret. |
| Webhooks arriving out of order causing duplicate processing. | Subscriber not deduplicating on event id. | Persist event id on receipt; ignore if seen. |
| Subscriber dead letter queue filling up overnight. | Subscriber down or rate-limiting. | Investigate subscriber; once recovered, click 'Replay all' on dead letter queue. |
| loan.delinquent never fires though loans clearly overdue. | Daily ageing cron disabled. | Settings > Technical > Scheduled Actions > MFI Loan Ageing > enable. |
See also
Was this page helpful?

