Back to Articles
Handling Dropped Safaricom Daraja STK Push Callbacks - An Idempotent Reconciliation Architecture

Handling Dropped Safaricom Daraja STK Push Callbacks - An Idempotent Reconciliation Architecture

Developers building on the Safaricom M-Pesa Daraja API quickly learn an unwritten rule of mobile payment orchestration: never assume a webhook callback will always arrive.

While Daraja's Lipa Na M-Pesa Online (STK Push) provides a seamless consumer payment prompt, network timeouts, cellular handoff latency, and ingress reverse proxy failures can cause asynchronous callbacks to be dropped, delayed, or delivered out of order.

When an STK Push callback drops, the customer sees money deducted from their M-Pesa balance, but your application marks the order as unpaid. Here is a production-grade blueprint for architecting idempotent reconciliation and automated fallback recovery.


Why STK Push Callbacks Drop

The standard Daraja STK Push lifecycle involves multiple decoupled asynchronous hops:

[Customer Phone] ── (PIN Entered) ──▶ [Safaricom Core Network]
                                             │
                                   (Asynchronous Webhook)
                                             │
                                             ▼
                               [Public Ingress / Load Balancer]
                                             │
                                   [Your Application Server]

Critical failure points include:

  1. Ingress Gateway Timeouts (504 Gateway Timeout): Daraja retries failed webhook deliveries with limited backoff. If your ingress server takes longer than 5 seconds to respond with HTTP 200, the callback may be discarded.
  2. Network Jitter & Mobile Handoff: The customer enters their PIN 45 seconds after initiating checkout. By then, the client-side polling session has expired.
  3. Payload Sanitization & PII Masking: In strict compliance pipelines, intermediate proxy logs hash customer mobile numbers (MSISDN) to SHA-256 to prevent plaintext telephone exposure. If a callback arrives malformed or unlinked to a checkout session, engineers cannot match the transaction without resolving the phone hash.

The 3-Tier Idempotent Reconciliation Architecture

To ensure zero lost transactions and eliminate manual support escalations, implement this three-tier recovery loop:

                  [STK Push Initiated]
                            │
               ┌────────────┴────────────┐
               ▼                         ▼
      [Webhook Arrives?]        [Background Polling Worker]
         ├── YES ──▶ Credit        └── Query Daraja Status API
         │           Ledger                     │
         └── NO                                 ▼
               │                    (Transaction Found?)
               ▼                       ├── YES ──▶ Match & Credit
    [Reconcile Audit Log]              └── NO ──▶ Wait 60s
               │
    [Query LOOKUP API for MSISDN]
               │
      [Match Account & Resolve]

Tier 1: Idempotent Webhook Handler

Every incoming callback must be processed idempotently using the unique CheckoutRequestID and MpesaReceiptNumber:

// Example: Safe idempotent callback processing
export async function handleMpesaCallback(payload: DarajaCallbackPayload) {
  const { CheckoutRequestID, ResultCode, CallbackMetadata } = payload.Body.stkCallback;
 
  if (ResultCode !== 0) {
    await markCheckoutFailed(CheckoutRequestID);
    return { ResultCode: 0, ResultDesc: "Accepted" };
  }
 
  const receipt = CallbackMetadata.Item.find(i => i.Name === "MpesaReceiptNumber")?.Value;
  const rawPhone = CallbackMetadata.Item.find(i => i.Name === "PhoneNumber")?.Value;
 
  // Atomically claim transaction lock to prevent double-crediting
  const acquired = await redis.set(`lock:mpesa:${receipt}`, "1", "NX", "EX", 300);
  if (!acquired) {
    return { ResultCode: 0, ResultDesc: "Duplicate Ignored" };
  }
 
  await creditCustomerAccount(receipt, CheckoutRequestID, rawPhone);
  return { ResultCode: 0, ResultDesc: "Success" };
}

Tier 2: Scheduled Query Status Fallback

If no callback is received within 90 seconds of transaction initiation, a background worker queries Daraja's Lipa Na M-Pesa Query API (POST /mpesa/stkpushquery/v1/query) using the original CheckoutRequestID.

  • If the response indicates success, the worker triggers ledger allocation.
  • If the response indicates pending user interaction, the job is requeued with exponential backoff (up to 3 minutes).

Tier 3: Forensic Log Matching with LOOKUP API

In distributed enterprise environments where telemetry buses (Kafka, RabbitMQ, or AWS SQS) store customer numbers as SHA-256 hashes for zero-trust compliance, orphaned transactions cannot be directly matched against customer phone numbers.

When unallocated M-Pesa receipts appear in banking settlement dumps:

  1. Extract the hashed customer identifier from the settlement log: 1235926f0e31f4c9cee58f172167753b2f94f7574fb89f7adfcbb8724fa0446c
  2. Perform a sub-millisecond reverse point lookup via LOOKUP API:
    curl -X GET "https://backend.zero-one-logistics.co.ke/v1/lookup/1235926f0e31f4c9cee58f172167753b2f94f7574fb89f7adfcbb8724fa0446c" \
      -H "X-API-Key: lk_live_your_key_here" \
      -H "API-Version: 2026-09-12"
  3. Receive the resolved phone number (254791000000) in 0.48ms and automatically allocate the payment to the appropriate customer wallet.

Production Checklist

  • Ensure all webhook endpoints return HTTP 200 OK within 1,500ms before performing heavy database updates.
  • Enforce distributed locks on MpesaReceiptNumber to prevent race conditions during retry bursts.
  • Implement automated fallback polling via Daraja Query API after 90 seconds.
  • Connect automated ledger reconciliation to LOOKUP API for sub-millisecond resolution of hashed audit logs.
⚡ Ready for production deployment

Start resolving 100M+ phone hashes today

Get instant self-service access with 500 RPS free capacity. No credit card required.

Handling Dropped Safaricom Daraja STK Push Callbacks - An Idempotent Reconciliation Architecture | LOOKUP API