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.

Payment providers
Configured providers — M-Pesa Daraja, Airtel, MTN with credentials.

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.
Warning — Safaricom takes 5–10 working days to whitelist a new C2B URL. Apply early. Without whitelisting, paybill deposits will succeed at Safaricom but never confirm into your tenant.

Configure the Daraja provider in BridgeERP

Live transactions
Every callback received, matched, posted.

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
Note — Sandbox testing phone +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.

  1. Member calls the branch or opens iBANK and chooses 'Pay loan'.
  2. System looks up the outstanding installment and pre-fills the amount.
  3. System sends an STK push to the member's phone (PUT /mpesa/stkpush/v1/processrequest).
  4. Member sees the prompt, enters PIN, confirms.
  5. Safaricom posts an STK callback to /mfi_mpesa/stk/callback.
  6. System matches the callback to the original request via the CheckoutRequestID.
  7. 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.

FieldFormatExampleRouting
PaybillNumeric522522Always the tenant paybill
Account numberLoan: L--L-KAK-001234Repays the loan
Account numberSavings: S--S-KAK-009876Deposits to savings
Account numberShare: SH-SH-12345Buys share capital
Warning — If a member enters a malformed account reference, the validation URL must reject the transaction (C2B v2 requires explicit accept/reject). The shipped validator parses the prefix and rejects with reason code C2B00012 'Invalid account number' — Safaricom shows this to the member as 'Invalid account, payment rejected'.

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.

BankConnectorUseCost
KCBkcb_iconnectRTGS, EFT, balance lookupKES 100 per RTGS
Co-op Bankcoop_omniRTGS, EFTKES 100 per RTGS
Equityequity_jengaPesaLink, RTGSKES 80 per PesaLink
NCBAncba_nowBalance + statement onlyFree

Go-live checklist

  1. Sandbox STK push + C2B + B2C all succeed with test phone +254708374149.
  2. Callback URLs whitelisted by Safaricom (email confirmation in hand).
  3. Production credentials encrypted at rest (check mfi.mpesa.provider uses BridgeERP's vault).
  4. Bank API connector tested with KES 100 transfer.
  5. Cron mfi_mpesa_reconcile running every 15 minutes.
  6. Two-person rule CASH_OUT extended to cover B2C above KES 100K.

Worked scenarios

Scenario — Jane repays her instalment via STK push

Setting: Tuesday morning. Jane owes KES 18,250 on her Emergency Loan. She is at the market and calls the Mumias branch to ask how to pay.

CharacterRole
Jane WambuiBorrower
Peter OtienoLoan officer at Mumias

Timeline

  1. 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)
  2. Day 1, 10:17: Peter clicks 'Request STK push'. System reads Jane's mobile, pre-fills KES 18,250. (STK request POSTed to Safaricom)
  3. Day 1, 10:17: Safaricom returns CheckoutRequestID. System stores it linked to the schedule line. (mfi.mpesa.transaction state=pending)
  4. Day 1, 10:18: Jane's phone buzzes. She sees 'Confirm payment of KES 18,250 to BRIDGE MFI'. Enters PIN. (External)
  5. Day 1, 10:18: Safaricom posts STK callback to /mfi_mpesa/stk/callback with ResultCode=0. (mfi.mpesa.transaction state=success)
  6. 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)
  7. Day 1, 10:19: Jane receives SMS confirmation: 'KES 18,250 received. 1 instalment to go.' (mail.template fired)
  8. 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

EndpointMethodUse
/oauth/v1/generateGETGet bearer token (1h TTL)
/mpesa/stkpush/v1/processrequestPOSTSTK push initiation
/mpesa/stkpushquery/v1/queryPOSTSTK status query
/mpesa/c2b/v2/registerurlPOSTRegister C2B validation and confirmation URLs
/mpesa/b2c/v3/paymentrequestPOSTB2C disbursement
/mpesa/transactionstatus/v1/queryPOSTTransaction status query (for reconciliation cron)
/mpesa/accountbalance/v1/queryPOSTPaybill balance for treasury

Common Safaricom result codes

CodeMeaningOperator action
0SuccessProcess normally
1Insufficient fundsMember needs to top up; retry
1032Request cancelled by userMember declined STK; ask why
1037Timeout, no user responseResend after a call to member
2001Wrong PINMember to retry; lockout after 3
3001Account does not existReject C2B; member to reconfirm account ref
404.001.03Invalid Access TokenToken expired; refresh OAuth

Troubleshooting

SymptomLikely causeFix
STK push returns 'Bad Request - Invalid PhoneNumber'Phone not in E.164 (254...) or shorthandNormalise via the shipped mfi_mpesa.utils.normalize_msisdn to 254XXXXXXXXX.
C2B deposits arrive but don't post to the loanAccount reference malformed or branch prefix unknownOpen the unmatched C2B queue; manually allocate; train members on the right format.
B2C result callback never arrivesResult URL behind firewall not whitelistedOpen 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 mismatchForce refresh via the shipped mfi.mpesa.provider.force_refresh_token() action.
Reconciliation cron shows 50+ pending transactionsCallback URL changed but not re-registered with SafaricomCall /mpesa/c2b/v2/registerurl again with the new URL.

See also

Was this page helpful?