Oct 11, 2026 · Payments · 5 min read
Integrating Safaricom Daraja in Laravel: C2B, B2C and reversals that don't lose money
Every software engineer building in Kenya eventually integrates Safaricom’s M-PESA Daraja API. On paper, the flow looks straightforward: your application triggers an STK push or registers C2B URLs, Safaricom sends a JSON payload to your webhook, and you credit the customer’s account or confirm their order. In production, however, silent failures, duplicate webhook retries, and network timeouts turn naive implementations into financial leaks.
When you handle millions of shillings across thousands of transactions, "it worked in sandbox" is not a valid strategy. In this article, I want to break down the architectural patterns and defensive practices I have used in production systems to make sure C2B collections, B2C payouts, and reversals never lose money.
1. Why Daraja Integrations Lose Money
Most integration issues do not come from Safaricom’s API being down; they come from engineers making optimistic assumptions about network reliability and distributed state. The most common failure modes include:
- Duplicate Webhook Delivery: The same callback can reach you more than once, for example when your endpoint is slow to respond or a proxy retries. Without idempotency guards, you may credit a customer's wallet or dispatch goods twice.
- Unprocessed Timeouts on STK Push: A user receives an STK prompt, enters their PIN, and Safaricom debits their phone, but your mobile app loses connectivity before receiving confirmation. If your backend relies solely on the client app telling it what happened, the customer is debited while your system marks the order as unpaid.
- Premature B2C Success State: Treating an initial synchronous HTTP 200 from the B2C endpoint as proof that money was sent. That response only acknowledges that the request was queued; the true outcome arrives asynchronously at your ResultURL, usually seconds later and occasionally much longer.
- Unlogged Raw Callbacks: Parsing incoming payloads directly into application models. If an unexpected schema change or validation exception occurs during parsing, the request fails with a 500 and the payment details exist nowhere in your system. You lose the audit trail you need to fix it.
2. Caching OAuth Access Tokens Correctly
Every call to Daraja requires a Bearer token generated from your Consumer Key and Consumer Secret. Daraja tokens are valid for 3,599 seconds (1 hour). Calling the oauth endpoint on every request introduces unnecessary latency and risks rate limiting.
Cache the token with a safety margin (for example, for 50 minutes instead of 60) so you never send an expired one. In Laravel the cache facade's remember method does this in a few lines.
3. C2B: Register URLs, Validation, and Raw Ingestion
Customer-to-Business (C2B) payments involve two endpoints: a Validation URL and a Confirmation URL. When a customer pays via Paybill or Till number, Safaricom always calls your Confirmation URL once the payment completes. The Validation URL is only called if external validation has been enabled for your shortcode (Safaricom activates it on request); in that case you can reject a payment, for example an unknown account number, before it goes through, and returning an "accepted" result code lets it proceed to confirmation.
The golden rule for webhooks is simple: store first, process later. Your controller should immediately store the raw request payload in a dedicated callbacks table, dispatch an asynchronous job, and return a fast 200 response.
4. B2C Payouts: Persist Before Sending
Business-to-Customer (B2C) payouts are used to send winnings, refunds, vendor withdrawals, or rider pay. Because payouts deduct directly from your organisation's utility balance, safety is paramount.
Before making the HTTP request to Daraja, create a ledger record in your database with a pending status, capturing an internal transaction reference and the intended recipient phone and amount. When Daraja accepts the request, save the returned ConversationID and OriginatorConversationID on that record.
When the ResultURL receives Safaricom's final callback, match it by ConversationID. If the result code is 0, mark the payout as successful; otherwise, mark it failed and immediately trigger logic to release or refund the user's balance.
5. True Idempotency at the Database Level
Never rely solely on application memory or cache flags to prevent duplicate credits. The database unique constraint is your ultimate guarantee. In your payments table, put a unique index on the M-PESA receipt number.
In your worker job, wrap ledger updates inside a database transaction. Two workers can still race on the same receipt, so treat a unique constraint violation as "already processed" rather than an error.
Because the insert and the credit share one transaction, a duplicate callback either fails on the unique index before any money moves, or the first delivery rolls back completely and the retry succeeds.
6. Reversals and Nightly Ledger Reconciliation
Customers pay the wrong Paybill or the wrong amount, and disputes happen. There are two kinds of reversal to plan for. When you reverse a payment through the Reversal API, the request is asynchronous like B2C: persist it first, then wait for the result on your ResultURL. When a reversal is processed through Safaricom customer care, your system does not get a callback for it at all; you only discover it in your statements.
To keep accounts balanced:
- For reversals you initiate, deduct the credited amount from the customer's balance only after a successful result callback. If their balance is already spent, flag the account for administrative follow-up.
- Run an automated nightly reconciliation command. Compare your local transaction ledger against the Daraja Transaction Status API or Safaricom's statements; this is also how customer-care reversals surface. Any discrepancy must notify engineering through an alert channel.
Production Checklist
- Store raw callback JSON before running any business or validation logic.
- Enforce a database-level unique constraint on the M-PESA receipt number.
- Respond to Safaricom webhooks immediately; process business logic in background queues.
- Cache OAuth tokens with a 10-minute buffer before expiration.
- Persist B2C requests in the database before calling the outbound endpoint.
- Never trust client-side claims of payment completion without verifying backend webhooks.
- Set up daily reconciliation scripts to audit balances against Safaricom statements.