Resarv Logo

Handling M-Pesa STK Push Callback Idempotency & Webhook Edge Security

Architectural Pattern: Serverless Rust Lambdas, Out-of-Order Callbacks, and HMAC Signature Validation

Written by Resarv Core Engineering Team
Handling M-Pesa STK Push Callback Idempotency & Webhook Edge Security System Architecture Illustration

Handling M-Pesa STK Push Callback Idempotency & Webhook Edge Security

In appointment booking platforms, sports court reservation systems, and mobile payment gateways across East Africa, M-Pesa STK Push (Lipa na M-Pesa Online) is the preferred payment method for instant customer checkout.

However, handling M-Pesa asynchronous webhooks reliably introduces significant distributed systems challenges:

  • Network Retries & Duplicate Callbacks: Mobile network delays can cause Safaricom servers to retry sending the same webhook callback multiple times.
  • Out-of-Order Delivery: A user may cancel an STK prompt or pay successfully while network packets arrive out of chronological order.
  • Double-Crediting Risk: Without strict idempotency controls, duplicate webhooks can credit a user’s wallet or lock a court reservation twice.

At Resarv, we solved these challenges using Serverless Rust Lambdas (provided.al2023), DynamoDB Idempotency Locks, and HMAC Signature Verification at the Edge.


πŸ”’ 1. Webhook Signature Verification at the Edge

Before accepting any incoming payment callback payload into our internal event stream, the incoming HTTP request is verified against the provider signature to prevent spoofing or unauthorized payload injection.

// Serverless Rust Lambda Signature Validation
use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

pub fill fn verify_webhook_signature(
    secret: &[u8],
    payload: &[u8],
    signature_header: &str,
) -> bool {
    let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC keys length error");
    mac.update(payload);
    
    let expected_mac = mac.finalize().into_bytes();
    let hex_signature = hex::encode(expected_mac);
    
    hex_signature.eq_ignore_ascii_case(signature_header)
}

By executing this verification in custom Rust provided.al2023 runtimes on AWS Lambda, signature checking completes in less than 2 milliseconds with sub-50ms cold starts.


⚑ 2. Idempotency Lock Pattern with DynamoDB

To guarantee that an M-Pesa transaction is processed exactly once, every incoming callback payload extracts the unique MpesaReceiptNumber or CheckoutRequestID and queries a centralized DynamoDB Idempotency Table using conditional writes.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Callback Processing Stage             β”‚ Idempotency Behavior                                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1. Webhook Arrival                    β”‚ Extract CheckoutRequestID & MpesaReceiptNumber.        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 2. Conditional Lock Check             β”‚ Perform PutItem with attribute_not_exists(CheckoutID). β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 3. If Lock Exists (Duplicate Retry)   β”‚ Return HTTP 200 OK immediately; bypass processing.     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 4. If Lock Acquired (First Time)      β”‚ Process court booking, send WhatsApp pass & payout.    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

DynamoDB Conditional Expression:

{
  "TableName": "MpesaIdempotencyLocks",
  "Item": {
    "CheckoutRequestID": {"S": "ws_CO_28082026_99211"},
    "Status": {"S": "PROCESSING"},
    "TTL": {"N": "1787948000"}
  },
  "ConditionExpression": "attribute_not_exists(CheckoutRequestID)"
}

If Safaricom retries the callback 5 seconds later due to a transient HTTP timeout, the conditional write fails gracefully with ConditionalCheckFailedException, returning a clean HTTP 200 OK to Safaricom while preventing duplicate court slot reservations or wallet double-credits.


πŸ“± 3. Partner Claim Codes (PDL-XXXXXX) & Instant WhatsApp Notifications

Once the STK push callback is verified and locked:

  1. Court Slot Confirmation: The court slot status transitions to BOOKED.
  2. Teammate Claim Code Generation: For doubles Padel/Tennis bookings, a 6-digit claim code (e.g. PDL-8829) is assigned so partners can claim their entry ticket.
  3. WhatsApp Relay Delivery: The sending Lambda publishes the event to our SNS WhatsApp relay, delivering a digital QR pass ticket directly to the customer’s phone.

πŸ“ˆ Summary

By pairing Serverless Rust Lambdas with DynamoDB conditional idempotency locks, Resarv eliminates payment double-crediting risks and processes high-volume M-Pesa STK push webhooks with 99.999% reliability.