Wire up the payment rail
A Kenyan MFI without M-Pesa is a museum. This page walks the Daraja v2 wiring — OAuth credentials, STK push, C2B validation, B2C disbursement — plus the bank journal hookup that closes the loop.

Daraja credentials
Before you start, the MFI Treasury team must have:
- A Lipa Na M-Pesa Paybill or Till from Safaricom Business (apply at daraja.safaricom.co.ke or via your relationship manager).
- A Daraja app registered in the developer portal with Consumer Key and Consumer Secret.
- Both sandbox and production credentials — never test against production with live phones.
- The B2C initiator name and security credential (encrypted via Safaricom's public key).
- The C2B validation URL and confirmation URL whitelisted by Safaricom support.
Configure the Daraja provider in BridgeERP

Open MFI → Configuration → Mobile money providers → New:
# Settings captured
name: Bridge MFI — M-Pesa
code: MPESA_BRIDGE
provider: safaricom_daraja
environment: sandbox # flip to production after UAT
paybill: 522522
consumer_key: <from portal>
consumer_secret: <from portal, encrypted at rest>
pass_key: <from portal — for STK push>
initiator_name: bridge_b2c
b2c_security_credential: <encrypted with Safaricom public key>
callback_url_validation: https://mfi.example.co.ke/mfi_mpesa/c2b/validation
callback_url_confirmation: https://mfi.example.co.ke/mfi_mpesa/c2b/confirmation
callback_url_stk: https://mfi.example.co.ke/mfi_mpesa/stk/callback
callback_url_b2c_result: https://mfi.example.co.ke/mfi_mpesa/b2c/result
callback_url_b2c_timeout: https://mfi.example.co.ke/mfi_mpesa/b2c/timeout
+254708374149 is safe for STK push tests. Real money does not move. Use it before flipping environment to production.STK push (member-initiated repayment)
STK push prompts a member's phone to confirm a payment. Used for repayments and deposits triggered from the branch or from iBANK.
- Member calls the branch or opens iBANK and chooses 'Pay loan'.
- System looks up the outstanding installment and pre-fills the amount.
- System sends an STK push to the member's phone (PUT /mpesa/stkpush/v1/processrequest).
- Member sees the prompt, enters PIN, confirms.
- Safaricom posts an STK callback to
/mfi_mpesa/stk/callback. - System matches the callback to the original request via the CheckoutRequestID.
- On success, the loan repayment is posted to the GL and the schedule advances.
C2B (paybill deposits)
C2B is for members who push payments themselves via the M-Pesa menu. The account reference must encode the loan number or savings account so the system can auto-allocate.
| Field | Format | Example | Routing |
|---|---|---|---|
| Paybill | Numeric | 522522 | Always the tenant paybill |
| Account number | Loan: L- | L-KAK-001234 | Repays the loan |
| Account number | Savings: S- | S-KAK-009876 | Deposits to savings |
| Account number | Share: SH- | SH-12345 | Buys share capital |
B2C (disbursement)
B2C pushes money from the MFI's paybill to a member's phone for loan disbursement. Use for loans up to KES 70,000 — above that, use bank transfer.
- Same-day limit per phone: KES 150,000 (Safaricom default; can be lifted).
- Per-transaction limit: KES 70,000 (Safaricom default).
- Fee: paid by the MFI; charged at Safaricom's published B2C tariff (~KES 75 for a KES 50K transfer).
- Settlement: T+0 to the member, T+1 to the MFI's settlement bank.
- Reconciliation: B2C Result callback must post within 60s or the system flags the transaction as
pending_confirmation.
Bank rail (RTGS, EFT, PesaLink)
For disbursements above KES 70,000 and treasury sweeps, configure at least one bank API. The shipped connectors:
Most tenants enable PesaLink (instant, KES 80, up to KES 999,999 per leg) for mid-size disbursements and RTGS for anything above.
| Bank | Connector | Use | Cost |
|---|---|---|---|
| KCB | kcb_iconnect | RTGS, EFT, balance lookup | KES 100 per RTGS |
| Co-op Bank | coop_omni | RTGS, EFT | KES 100 per RTGS |
| Equity | equity_jenga | PesaLink, RTGS | KES 80 per PesaLink |
| NCBA | ncba_now | Balance + statement only | Free |
Go-live checklist
- Sandbox STK push + C2B + B2C all succeed with test phone
+254708374149. - Callback URLs whitelisted by Safaricom (email confirmation in hand).
- Production credentials encrypted at rest (check
mfi.mpesa.provideruses BridgeERP's vault). - Bank API connector tested with KES 100 transfer.
- Cron
mfi_mpesa_reconcilerunning every 15 minutes. - Two-person rule CASH_OUT extended to cover B2C above KES 100K.
Worked scenarios
Scenario — Jane repays her instalment via STK push
| Character | Role |
|---|---|
| Jane Wambui | Borrower |
| Peter Otieno | Loan officer at Mumias |
Timeline
- Day 1, 10:15: Jane calls. Peter opens her loan and confirms instalment 2 of 3 is due tomorrow. (mfi.loan.schedule line 2 due)
- Day 1, 10:17: Peter clicks 'Request STK push'. System reads Jane's mobile, pre-fills KES 18,250. (STK request POSTed to Safaricom)
- Day 1, 10:17: Safaricom returns CheckoutRequestID. System stores it linked to the schedule line. (mfi.mpesa.transaction state=pending)
- Day 1, 10:18: Jane's phone buzzes. She sees 'Confirm payment of KES 18,250 to BRIDGE MFI'. Enters PIN. (External)
- Day 1, 10:18: Safaricom posts STK callback to /mfi_mpesa/stk/callback with ResultCode=0. (mfi.mpesa.transaction state=success)
- Day 1, 10:18: System matches callback to schedule line 2, posts repayment, advances loan to instalment 3 of 3. (mfi.loan.repayment created, account.move posted)
- Day 1, 10:19: Jane receives SMS confirmation: 'KES 18,250 received. 1 instalment to go.' (mail.template fired)
- Day 1, 10:20: Peter confirms the payment shows on his screen. (Branch view live)
Outcome — Repayment completed end-to-end in under five minutes with zero manual reconciliation.
Reference
Daraja v2 endpoints used
| Endpoint | Method | Use |
|---|---|---|
| /oauth/v1/generate | GET | Get bearer token (1h TTL) |
| /mpesa/stkpush/v1/processrequest | POST | STK push initiation |
| /mpesa/stkpushquery/v1/query | POST | STK status query |
| /mpesa/c2b/v2/registerurl | POST | Register C2B validation and confirmation URLs |
| /mpesa/b2c/v3/paymentrequest | POST | B2C disbursement |
| /mpesa/transactionstatus/v1/query | POST | Transaction status query (for reconciliation cron) |
| /mpesa/accountbalance/v1/query | POST | Paybill balance for treasury |
Common Safaricom result codes
| Code | Meaning | Operator action |
|---|---|---|
| 0 | Success | Process normally |
| 1 | Insufficient funds | Member needs to top up; retry |
| 1032 | Request cancelled by user | Member declined STK; ask why |
| 1037 | Timeout, no user response | Resend after a call to member |
| 2001 | Wrong PIN | Member to retry; lockout after 3 |
| 3001 | Account does not exist | Reject C2B; member to reconfirm account ref |
| 404.001.03 | Invalid Access Token | Token expired; refresh OAuth |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| STK push returns 'Bad Request - Invalid PhoneNumber' | Phone not in E.164 (254...) or shorthand | Normalise via the shipped mfi_mpesa.utils.normalize_msisdn to 254XXXXXXXXX. |
| C2B deposits arrive but don't post to the loan | Account reference malformed or branch prefix unknown | Open the unmatched C2B queue; manually allocate; train members on the right format. |
| B2C result callback never arrives | Result URL behind firewall not whitelisted | Open inbound to Safaricom IPs 196.201.214.x/24; confirm with nc -lv 80. |
| OAuth fails with 'Invalid Authentication Token' | Token expired (1h TTL) or sandbox/production mismatch | Force refresh via the shipped mfi.mpesa.provider.force_refresh_token() action. |
| Reconciliation cron shows 50+ pending transactions | Callback URL changed but not re-registered with Safaricom | Call /mpesa/c2b/v2/registerurl again with the new URL. |

