LIVE
1.29°S / 36.82°E  ·  Nairobi

M-Pesa STK Push in Python: A Production Guide

Restoration note, September 3, 2026: This article was first published on March 18, 2026. The restoration preserves its production focus while removing unaudited claims about fixed field limits, callback retries, security headers, IP ranges, sandbox numbers, result-code meanings, and a two-minute failure deadline. Confirm product-specific requirements in your current Daraja portal and contract before release.

There are tutorials that show you how to get an STK Push working in a sandbox. This is about the parts that break in real conditions: expired credentials, ambiguous network failures, duplicate callbacks, missing callbacks, concurrent retries, and fulfilment that runs twice.

The central rule is simple: a missing callback is an unknown state, not proof of failure.

What STK Push actually does

An M-Pesa Express request initiates a customer payment interaction. The immediate HTTP response acknowledges whether the request was accepted for processing; the final result arrives asynchronously through the callback flow or can be investigated through the STK query endpoint. Safaricom's public SDK materials expose both the initiation and query operations. ([github.com](https://github.com/safaricom/mpesa-php-sdk))

Model that as a state machine rather than one function call. Useful application states include created, initiating, pending, succeeded, failed, and unknown. Only succeeded and a verified terminal failure should be treated as final.

Persist the payment attempt before making the request

Create a durable payment-attempt record before calling Daraja. Store your internal attempt ID, order ID, expected amount, normalized phone number, shortcode configuration, creation time, and current state. Enforce a database rule that prevents two unresolved attempts for the same order unless duplicate payment prompts are an intentional business feature.

Initiation sequence: lock the order; reject or return an existing unresolved attempt; create the attempt; commit it; call Daraja; then persist the returned merchant and checkout identifiers if the request is accepted.

This ordering matters. If the process dies after Daraja accepts the request but before your application records it, the callback may arrive for a transaction your database cannot identify. Your callback inbox must therefore also tolerate an event arriving before the initiation worker finishes updating the attempt.

The token problem, corrected

Daraja authentication uses the client-credentials endpoint and a bearer access token. The token response may include an expires_in value; use that response value rather than hard-coding a lifetime. Safaricom's public SDK shows the Basic-authenticated OAuth endpoint, while captured Daraja response documentation shows the access-token and expiry fields. ([github.com](https://github.com/safaricom/mpesa-java-sdk/blob/master/src/main/java/Mpesa.java))

The original article said to catch a 401 and refresh, but its sample only refreshed according to the cached expiry. A production client should do both.

Token-cache algorithm: return the cached token while the monotonic clock is earlier than its calculated expiry minus a small safety window. Otherwise request a new token and cache its expiry.

Authenticated-call algorithm: send the request once. If the response is HTTP 401, invalidate the cached token, obtain another token, and retry once. Do not turn that narrow rule into a general retry policy for payment initiation.

Use a monotonic clock for elapsed-time calculations, protect refreshes with a lock to prevent a fleet of workers minting tokens simultaneously, and keep consumer credentials outside source control. Never log the consumer secret, passkey, bearer token, generated password, or complete callback URL if that URL contains a secret.

The password and request payload

The STK password is not the consumer secret. It is the Base64 encoding of the shortcode, Lipa Na M-Pesa passkey, and timestamp concatenated without separators. The exact same timestamp must appear in the request payload. Safaricom material describes this construction and the principal request fields. ([safaricom.co.ke](https://www.safaricom.co.ke/images/Downloads/Tender_Documents/EOI_Safaricom_M-PESA_Integration_V1_002.pdf?utm_source=openai))

Python shape: create one timestamp string; concatenate shortcode + passkey + timestamp; encode the resulting bytes with Base64; return the password and timestamp together.

Synchronize the host clock and generate the password immediately before the request. Do not calculate the timestamp twice.

The initiation payload includes the business shortcode, generated password, timestamp, transaction type, amount, paying party, receiving party, phone number, callback URL, account reference, and transaction description. The sandbox and production hosts, initiation endpoint, and query endpoint should be configuration rather than values scattered through business code. Safaricom's SDK examples use the STK initiation path and the separate query path. ([github.com](https://github.com/safaricom/mpesa-java-sdk/blob/master/src/main/java/Mpesa.java))

Validate the amount as a positive whole-number business value before serialization. Parse and normalize the phone number with an actual validation function; do not merely strip a plus sign or replace the first zero. Public samples commonly use country-code form, but your accepted prefixes and shortcode behavior should come from current product configuration rather than a copied regular expression. ([github.com](https://github.com/safaricom/mpesa-node-library))

The original article asserted fixed 12- and 13-character limits for the account reference and description. Those limits were not established during this review, so they should not be presented as universal facts. Validate both fields against the schema and credentials currently shown in your Daraja environment.

Network timeouts are not ordinary failures

Always configure separate connection and read timeouts. Python Requests supports a timeout pair, and distinguishes connection timeouts from read timeouts. A read timeout means no response arrived within the allowed period; it does not prove that the remote system did nothing. ([docs.python-requests.org](https://docs.python-requests.org/en/stable/api/?utm_source=openai))

An STK initiation is a state-changing POST. HTTP guidance warns against automatically retrying non-idempotent requests unless the client can establish that the original operation was not applied or has application-specific recovery semantics. ([httpwg.org](https://httpwg.org/specs/rfc9110.html?utm_source=openai))

Safe classification: validation rejection is failed; an accepted response is pending; HTTP 401 gets one token refresh and retry; a failure known to occur before connection establishment may be retried under a bounded policy; a read timeout or dropped response after transmission becomes unknown.

Do not hide this distinction behind a broad retry decorator. Retrying an ambiguous initiation can send a second prompt and can produce a second charge.

The callback is an inbox, not a controller action

Daraja callback examples place identifiers and the result under a nested Body and stkCallback object. Successful examples include callback metadata such as amount, receipt number, transaction date, and phone number; failure-shaped callbacks may omit that metadata. Parse by field name, tolerate missing optional values, and preserve the raw body for audit and later parser improvements. ([github.com](https://github.com/mboya/daraja-mcp?utm_source=openai))

The callback handler should perform only bounded work before responding: limit body size, parse JSON, validate the envelope, store a redacted copy in a durable inbox, deduplicate it, associate it with a payment attempt, and commit. Fulfilment should run from a durable queue or transactional outbox after that commit.

Idempotent callback transaction: acquire the attempt row; insert the callback under a unique event key; return success immediately if it was already accepted; compare identifiers and expected payment attributes; apply a legal state transition; write an outbox event; commit.

Do not assume callbacks arrive once or in order. A duplicate success must not dispatch goods twice. A late failure must not reverse a success automatically. A callback for an unknown checkout identifier should be quarantined for reconciliation rather than discarded.

Return a successful HTTP response only after the callback has been durably accepted. Do not perform email, inventory allocation, external fulfilment, or other slow work inside the request transaction.

Verifying the callback

A secret embedded in a callback path is a bearer secret: anyone who learns the URL can use it. A constant-time comparison is appropriate when comparing secrets, but it does not prove that Safaricom created the request. The original article overstated this mechanism as source verification.

Use a long random callback token as one defensive layer, terminate TLS correctly, restrict request methods and content types, impose body and rate limits, and rotate the token when exposure is suspected. If your current Safaricom agreement documents a signature, certificate, or source-address control, implement it exactly as documented and test its failure modes. This review did not establish a public, stable STK callback-signature contract, so none is invented here.

Most importantly, treat the callback as a claim about an existing attempt. Match its checkout and merchant identifiers to stored values, compare the amount and phone where supplied, enforce receipt uniqueness, and use an authenticated outbound status query when independent confirmation is required before irreversible fulfilment.

Querying and reconciling transaction status

Safaricom's public SDKs expose an STK status query using the checkout request identifier returned by the initiation flow. ([github.com](https://github.com/safaricom/mpesa-php-sdk))

Run reconciliation for pending and unknown attempts on a bounded backoff schedule. Stop aggressive polling after a configurable window, but do not relabel an unresolved payment as failed merely because a clock expired. Move it to unknown, continue lower-frequency reconciliation where operationally justified, and prevent an unguarded retry.

The query response is another external observation, not permission to overwrite history blindly. Record the raw result and transition state only when the new evidence is compatible with the existing state. Conflicting terminal evidence belongs in an exception queue for investigation.

Python service boundaries

Keep the integration small and explicit. A useful structure has a credential provider, Daraja HTTP client, payment-attempt repository, callback-inbox repository, reconciliation worker, and fulfilment worker.

DarajaAuth: obtains and caches the token, uses the response expiry, coordinates refreshes, and performs one 401 refresh.

DarajaClient: builds passwords and payloads, owns timeouts, parses HTTP responses, and never silently retries an ambiguous initiation.

PaymentService: validates business input, creates the durable attempt, prevents concurrent duplicate attempts, and records returned identifiers.

CallbackService: stores, deduplicates, validates, and applies state transitions without performing fulfilment inline.

Reconciler: queries unresolved attempts with bounded backoff and sends contradictions or long-lived unknowns to operations.

A shared Requests session provides connection pooling. Keep TLS certificate verification enabled. Classify JSON-decoding failures separately from HTTP failures, retain a safely truncated response body for diagnosis, and attach your internal attempt ID to structured logs. Requests documents sessions, timeout behavior, TLS verification, and its exception hierarchy. ([docs.python-requests.org](https://docs.python-requests.org/en/stable/api/?utm_source=openai))

Tests that belong in the build

Test token expiry, concurrent token refresh, and the one-time 401 path. Test that the password and payload use the same timestamp. Test malformed responses, missing callback metadata, duplicate callbacks, callbacks arriving before initiation persistence completes, conflicting terminal results, and two workers trying to initiate the same order.

Add failure-injection tests for connection timeout, read timeout after transmission, process termination after Daraja accepts the request, and process termination after callback persistence but before fulfilment dispatch. The system should recover without losing the payment or fulfilling it twice.

Finally, run contract tests against the sandbox credentials assigned to your application. Do not freeze one tutorial's test phone, shortcode, passkey, field limits, or result-code descriptions into production logic.

Packages associated with the original article

The original project remains available as mpesa-mcp, with a corresponding PyPI package. The package listing continued to receive releases in August 2026. ([github.com](https://github.com/gabrielmahia/mpesa-mcp?utm_source=openai))

The Python SDK package daraja-v3 and test double daraja-mock were published on March 18, 2026. Consult their current project pages and source before adopting them; package availability is not a substitute for your own security review, integration tests, and reconciliation design. ([pypi.org](https://pypi.org/project/daraja-v3/))

Gabriel Mahia builds decision infrastructure for East Africa. Engineering blog: aikungfu.dev. Portfolio: gabrielmahia.github.io.

Responses