M-Pesa integration · Kenya

M-Pesa Daraja integration done properly, in production.

The M-Pesa sandbox is forgiving. Production is not. Integrations that pass every sandbox test routinely fail on go-live because callback URLs time out, duplicate payment notifications are processed twice, or a silent API error leaves a transaction in an indeterminate state that neither the customer nor the ledger can resolve. We have been through that terrain, not in a tutorial, but in production systems that move real money for real people. Our M-Pesa payroll module inside MkulimaOS disburses bulk worker wages via the Daraja API from a single dashboard action — every disbursement is idempotent, every result is reconciled, and every failure surfaces an actionable error rather than a silent void. We also integrated M-Pesa giving (alongside Stripe for diaspora donors) into the ACK Diocese platform. When we wire Daraja for a client, we are drawing on systems we maintain, not patterns we read about.

In production

We run M-Pesa payroll inside MkulimaOS — bulk worker disbursement via the Daraja B2C API from a single dashboard action, with a full audit trail and automated reconciliation. We also built M-Pesa + Stripe giving into the ACK Diocese of Kitale platform. Both systems are live and processing real transactions.

See where it runs

STK Push (Lipa na M-Pesa Online): customer-facing checkout done right

STK Push is the most common Daraja integration request, and also the most commonly broken one. The API call that initiates a push is straightforward. The complexity sits in everything that follows: the callback that Safaricom posts to your server must be reachable from the public internet, must respond within a narrow window, and must handle the full range of result codes — not just success. We structure STK Push integrations with a dedicated callback endpoint that validates the incoming payload signature, writes a transaction record in a pending state before the push is even initiated, and transitions that record to confirmed or failed based on the callback result. If Safaricom never delivers a callback — which happens — the integration polls the transaction status API on a configurable schedule rather than leaving a payment dangling. The customer sees a definitive outcome. Your ledger matches.

  • Dedicated callback endpoint with payload validation and idempotency key enforcement
  • Pending transaction record written before STK Push is initiated
  • Automated status polling for callbacks that never arrive
  • Full result-code handling — including cancellations, timeouts, and insufficient funds

C2B and B2C: paybill/till collection and bulk payouts

C2B (customer-to-business) handles payments made by customers to your paybill number or till. The integration registers your confirmation and validation URLs with Safaricom, receives incoming payment notifications, and matches them to open orders or accounts in your system. Validation URLs let you accept or reject a payment before it settles — useful when you need to verify an account reference before funds land. B2C (business-to-customer) runs in the opposite direction: your system initiates a payout to a customer or worker's phone number. This is the flow that powers our production payroll in MkulimaOS. Payroll totals are calculated from attendance and piece-rate records, the B2C request is initiated with a generated originator conversation ID, and the result callback updates the worker's disbursement record. We enforce one-disbursement-per-payroll-period at the database level, so a network retry cannot result in a double payment. The Daraja API returns both synchronous acknowledgements and asynchronous result callbacks — our integrations handle both, rather than treating the acknowledgement as confirmation.

The hard part: callbacks, reconciliation, idempotency, and timeouts

This section is the reason most M-Pesa integrations break on go-live, and it is the part that most tutorials skip. Safaricom's callback delivery is eventually consistent, not guaranteed. Your callback endpoint can receive a notification twice, out of order, or not at all. An integration that processes callbacks naively will double-credit accounts on duplicate delivery and leave transactions unresolved when delivery fails. We approach this with a set of patterns we have proven in production. Every initiated transaction is assigned a unique merchant request ID before the API call is made. The callback handler checks that ID against a processed-transactions table before taking any action — if the ID is already there, the callback is acknowledged and discarded without re-processing. If a callback has not arrived within a configurable window, a background job queries the Daraja transaction status endpoint and reconciles the result directly. Timeouts on outbound API calls are handled explicitly: a request that times out is not assumed to have failed, because the payment may have been initiated on Safaricom's side before the connection dropped. The correct response is to query status — not to retry the initiation, which risks a double charge.

  • Idempotency key stored before API call; callback handler checks before acting
  • Duplicate callback detection via processed-transactions table
  • Background reconciliation job for callbacks that never arrive
  • Explicit timeout handling: status query, not blind retry
  • Transaction state machine: pending → confirmed / failed / refunded

Sandbox to production: Daraja go-live, credentials, and integrating into your app

The Daraja sandbox and production environments use different credential sets, different base URLs, and occasionally different API behaviour. The go-live process requires submitting your integration for Safaricom's review, which involves documenting your use case, configuring your shortcode, and ensuring your callback URLs are publicly reachable and return the correct response format. We have done this process multiple times and can guide you through it or handle it end-to-end. For integrations into existing systems, we treat your codebase as the integration surface: we read your stack, identify where payment initiation and confirmation need to plug in, and write the integration layer to match your conventions rather than introducing a foreign pattern. For greenfield projects, we scaffold a clean service layer — Daraja client, transaction repository, reconciliation job — that is testable independently of the M-Pesa API itself, using Safaricom's sandbox for end-to-end verification. Everything we write is delivered with documentation, and you own it on delivery. There is no Spidey Labs dependency in your payment path.

What it covers

The modules, end to end.

STK Push / Lipa na M-Pesa Online

Customer-initiated payment prompts with full callback handling, status polling, and result-code coverage.

C2B (paybill / till)

Paybill and till collection with validation URL support, incoming payment matching, and ledger reconciliation.

B2C bulk payouts

Business-to-customer disbursement — worker payroll, vendor payments, cash-back — with idempotency guards and audit trail.

Transaction status & reconciliation

Automated status queries and background reconciliation for transactions without callbacks, so your ledger never diverges from Safaricom's.

Webhook / callback handling + idempotency

Callback endpoint hardened against duplicate delivery, out-of-order arrival, and timeout edge cases — the patterns that break naive integrations on go-live.

Sandbox → production go-live support

Credential configuration, Safaricom review submission, callback URL verification, and environment cutover — handled alongside or on your behalf.

Questions

Frequently asked.

What is the difference between STK Push, C2B, and B2C — and which one does my project need?
STK Push (Lipa na M-Pesa Online) sends a payment prompt to a customer's phone and waits for them to enter their PIN — it is the right choice for e-commerce checkout, subscription billing, and any flow where a customer initiates a payment in your app. C2B (customer-to-business) handles payments that customers make directly to your paybill or till number via their M-Pesa menu — useful for offline payment flows, utility bills, and walk-in payments that you want to reconcile automatically. B2C (business-to-customer) runs in the opposite direction: your system pushes money to a customer or worker's phone — the right choice for payroll, refunds, agent cash-outs, and disbursements. Most production systems need more than one: an e-commerce platform typically uses STK Push for checkout and B2C for refunds; a payroll system uses B2C for disbursements and may use C2B to collect employee contributions. We scope which flows you actually need in the first conversation, and we do not charge you for flows your product does not use.
How do you handle callbacks and reconciliation reliably? What happens if Safaricom's callback never arrives?
Every transaction we initiate is recorded in a pending state before the Daraja API call is made, with a unique merchant request ID. When Safaricom's callback arrives, the handler checks that ID against a processed-transactions table — if the ID is already marked processed, the callback is acknowledged and discarded without re-processing. This eliminates double-crediting on duplicate delivery. If no callback arrives within a configurable window (typically 2–5 minutes), a background job queries the Daraja QueryTransactionStatus endpoint and updates the transaction record based on the authoritative result from Safaricom's side. The customer or worker sees a definitive outcome, and your ledger stays accurate regardless of callback delivery.
Can you add M-Pesa to an app or system we already have — not a greenfield build?
Yes, and this is the more common scenario. We read your existing codebase, identify where payment initiation and settlement need to integrate, and write a service layer that follows your conventions — your language, your framework, your database schema. We do not require you to adopt a new architecture to accommodate M-Pesa. If you have an existing payments module, we extend it. If this is your first payment integration, we design the module to fit naturally alongside the rest of your code. We have integrated Daraja into Node.js, Next.js, NestJS, and Rails backends, and the approach is the same in each case: clean, testable, documented, and delivered as source code you own.
What does the sandbox-to-production go-live process look like, and how long does it take?
Safaricom's go-live process requires you to submit your integration for review through the Daraja portal. This involves documenting your business use case, confirming your shortcode configuration, and verifying that your callback URLs are publicly reachable and return the expected response format. Safaricom typically reviews and approves within two to five business days, though timelines vary. We handle the sandbox integration first — full end-to-end testing using Safaricom's test credentials and test phone numbers — and only move to production credentials once every flow has been verified in the sandbox. We prepare and submit the go-live documentation as part of the engagement, and we stay available through the cutover to address any issues that surface during the first live transactions.
What does an M-Pesa integration cost, and how long does it take?
A focused STK Push integration for an existing app — callback handling, reconciliation, go-live support, and documentation — typically takes one to two weeks and falls within our Sprint tier from $4,500. Multi-flow integrations combining STK Push, C2B, and B2C with a full reconciliation layer and payroll-grade audit trail run longer and are scoped as part of a Project engagement ($15K–$75K depending on complexity and the surrounding system). If you need ongoing Daraja support — credential rotations, result-code edge cases, new flow additions — that fits a Retainer from $3,500/month. We scope every engagement before billing begins, we sign an NDA before discussing your system specifics, and full IP transfers on delivery. We are Nairobi-based and serve clients in Kenya, the US, EU, and UK.

Build it properly

Tell us what your operation needs.

Fixed-scope, fixed-fee phases. Full IP transfer on delivery. We respond within one working day, and there's an NDA before any specifics.