Account Balance
Request account balances, receive asynchronous result notifications, and handle timeout callbacks from Safaricom's Account Balance API.
User Story
Who can use this service
As a finance or operations user, I want to query my organization's M-Pesa account balance so I can reconcile payments and spot problems quickly.
- Finance analyst: runs balance checks and reconciliation reports.
- Till manager / cashier: monitors till balances and requests top-ups when low.
- Backend system / automation: polls balances regularly and creates alerts or workflows on anomalies.
This guide documents the Account Balance flow: how to construct queries (sync and async), expected acknowledgement responses, how to parse ResultURL callbacks (final balances) and QueueTimeOutURL notifications using the SDK's built-in callback helpers, and important validation/safety checks to implement on your side.
Parameters Definition
| Parameter | Type | Description |
|---|---|---|
Initiatorrequired str | String | Username used to initiate the request (API initiator identity). |
SecurityCredentialrequired str | String | Encrypted security credential (as required by Safaricom). |
CommandIDrequired str | String | Transaction type. Default: 'AccountBalance'. |
PartyArequired int | Integer | Organization shortcode, till number or MSISDN depending on IdentifierType. |
IdentifierTyperequired int | Integer | Type of PartyA. Allowed values: 1 (MSISDN), 2 (Till number), 4 (Short code). |
Remarksrequired str | String | Freeform comments for the transaction (must not exceed 100 characters). |
QueueTimeOutURLrequired str | String | Callback URL for timeout notifications (QueueTimeOutURL). |
ResultURLrequired str | String | Callback URL where final account balance result will be posted. |
Response Schema
AccountBalanceResponse — the synchronous acknowledgement returned by client.balance.query(...)
| Parameter | Type | Description |
|---|---|---|
OriginatorConversationIDrequired str | null | String | Unique identifier for the original request, generated by the API gateway. |
ConversationIDrequired str | null | String | Unique identifier for the transaction, generated by M-Pesa. |
ResponseCoderequired str | int | String/Integer | Acknowledgement status code. Any all-zero value (e.g. '0', '000') indicates the request was accepted. |
ResponseDescriptionrequired str | String | Human readable acknowledgement message, e.g. 'Accept the service request successfully'. |
Result Callback Schema
AccountBalanceResultCallback — posted to ResultURL once the balance query completes
| Parameter | Type | Description |
|---|---|---|
Result.ResultTyperequired int | Integer | 0 = success, 1 = still waiting/timeout. |
Result.ResultCoderequired int | str | Integer/String | 0 indicates the balance query succeeded; any other value is a failure code. |
Result.ResultDescrequired str | String | Human readable result description. |
Result.OriginatorConversationIDrequired str | String | Matches the OriginatorConversationID from the initial acknowledgement. |
Result.ConversationIDrequired str | String | Matches the ConversationID from the initial acknowledgement. |
Result.TransactionID str | null | String | M-Pesa transaction ID for this balance query, when available. |
Result.ResultParameter.ResultParameters list[{Key, Value}] | Array | List of key/value pairs. The 'AccountBalance' key holds a pipe- and ampersand-delimited balance string per account (Working, Float, Utility, Charges Paid, Settlement); 'BOCompletedTime' holds the completion timestamp. |
Result.ReferenceData.ReferenceItem.Key str | String | Reference parameter name, e.g. 'QueueTimeoutURL'. |
Result.ReferenceData.ReferenceItem.Value str | String | Reference parameter value, e.g. the original QueueTimeoutURL you supplied. |
Timeout Callback Schema
AccountBalanceTimeoutCallback — posted to QueueTimeOutURL if the query doesn't complete in time
| Parameter | Type | Description |
|---|---|---|
Result.ResultTyperequired int | Integer | 1 for a timeout notification. |
Result.ResultCoderequired int | str | Integer/String | Non-zero code identifying the timeout. |
Result.ResultDescrequired str | String | e.g. 'The service request timed out.' |
Result.OriginatorConversationIDrequired str | String | Matches the OriginatorConversationID from the initial acknowledgement. |
Result.ConversationIDrequired str | String | Matches the ConversationID from the initial acknowledgement. |
Callback Acknowledgement Schemas
What your webhook handler should return to Safaricom
| Parameter | Type | Description |
|---|---|---|
ResultCode int | str | Integer/String | Defaults to 0. Used by AccountBalanceResultCallbackResponse and AccountBalanceTimeoutCallbackResponse — the typed models to return from your /result and /timeout handlers respectively. |
ResultDesc str | String | Defaults to 'Result received and processed successfully.' (result) or 'Timeout notification received and processed successfully.' (timeout). |
Overview
Query lifecycle: send AccountBalance request → receive immediate ack → process asynchronous result or timeout.
- Use the high-level MpesaClient/BalanceService facade for token management, header injection and typed Pydantic models.
- Use the lower-level AccountBalance class with a TokenManager and HttpClient if you need full control over request construction, middleware and error handling.
- Both the facade and the direct service are available as sync and async variants (
MpesaClient/AsyncMpesaClient,AccountBalance/AsyncAccountBalance). Reach for async when you're querying from inside an async web framework, or want to fire off balance checks for several shortcodes concurrently.
The facade handles authentication and returns typed models (acknowledgement and parsed callbacks). Use it for concise integration in most applications.
Quick Setup (Sync)
from mpesakit import MpesaClientfrom mpesakit.account_balance import AccountBalanceIdentifierType
# ---- Client / Request (synchronous example) ----client = MpesaClient(consumer_key="YOUR_KEY", consumer_secret="YOUR_SECRET", environment="sandbox")
resp = client.balance.query( initiator="apiuser", security_credential="ENCRYPTED_SECURITY_CREDENTIAL", party_a=600000, identifier_type=AccountBalanceIdentifierType.SHORT_CODE, remarks="Balance inquiry", result_url="https://your.service/webhook/account-balance/result", queue_timeout_url="https://your.service/webhook/account-balance/timeout",)
if resp.is_successful: print("Acknowledgement accepted:", resp.ConversationID or resp.OriginatorConversationID)else: print("Acknowledgement failed:", resp.ResponseDescription)Quick Setup (Async)
AsyncMpesaClient exposes the same balance.query(...) call as a coroutine. Use it inside async def route handlers, or when triggering balance checks for multiple shortcodes/tills at once with asyncio.gather.
import asynciofrom mpesakit import AsyncMpesaClientfrom mpesakit.account_balance import AccountBalanceIdentifierType
async def main(): async with AsyncMpesaClient( consumer_key="YOUR_KEY", consumer_secret="YOUR_SECRET", environment="sandbox", ) as client: resp = await client.balance.query( initiator="apiuser", security_credential="ENCRYPTED_SECURITY_CREDENTIAL", party_a=600000, identifier_type=AccountBalanceIdentifierType.SHORT_CODE, remarks="Balance inquiry", result_url="https://your.service/webhook/account-balance/result", queue_timeout_url="https://your.service/webhook/account-balance/timeout", )
if resp.is_successful: print("Acknowledgement accepted:", resp.ConversationID or resp.OriginatorConversationID) else: print("Acknowledgement failed:", resp.ResponseDescription)
asyncio.run(main())In a long-lived app (e.g. AsyncMpesaClient wired up as a FastAPI dependency), construct it once at startup and call await client.aclose() on shutdown rather than opening a new async with block per request.
Webhook Handling (Result & Timeout)
Instead of manually constructing AccountBalanceResultCallback / AccountBalanceTimeoutCallback models from the raw payload, use the client's built-in process_account_balance_callback and process_account_balance_timeout helpers. They validate and parse the payload for you and are available on both MpesaClient and AsyncMpesaClient since they only do validation, not I/O.
from fastapi import FastAPI, Request, HTTPExceptionfrom mpesakit import MpesaClientfrom mpesakit.account_balance import ( AccountBalanceResultCallbackResponse, AccountBalanceTimeoutCallbackResponse,)from mpesakit.security.ip_whitelist import is_mpesa_ip_allowedimport logging
app = FastAPI()logger = logging.getLogger("mpesa.account_balance")client = MpesaClient(consumer_key="YOUR_KEY", consumer_secret="YOUR_SECRET", environment="sandbox")
async def _caller_ip(request: Request) -> str: return (request.headers.get("x-forwarded-for") or request.client.host).split(",")[0].strip()
@app.post("/webhook/account-balance/result")async def account_balance_result(request: Request): payload = await request.json() caller_ip = await _caller_ip(request) if not is_mpesa_ip_allowed(caller_ip): raise HTTPException(status_code=403, detail="forbidden")
try: # Validates and parses the payload into an AccountBalanceResultCallback object callback = client.process_account_balance_callback(payload) except Exception as exc: logger.exception("Invalid AccountBalance result payload") return AccountBalanceResultCallbackResponse(ResultCode=1, ResultDesc=f"Invalid payload: {exc}")
logger.info("AccountBalance result received: %s", callback.model_dump(mode="json")) # e.g. extract and persist the AccountBalance entry from callback.Result.ResultParameters here
return AccountBalanceResultCallbackResponse() # defaults to ResultCode=0, ResultDesc="Result received and processed successfully."
@app.post("/webhook/account-balance/timeout")async def account_balance_timeout(request: Request): payload = await request.json() caller_ip = await _caller_ip(request) if not is_mpesa_ip_allowed(caller_ip): raise HTTPException(status_code=403, detail="forbidden")
try: # Validates and parses the payload into an AccountBalanceTimeoutCallback object callback = client.process_account_balance_timeout(payload) except Exception as exc: logger.exception("Invalid AccountBalance timeout payload") return AccountBalanceTimeoutCallbackResponse(ResultCode=1, ResultDesc=f"Invalid payload: {exc}")
logger.warning("AccountBalance timeout received: %s", callback.model_dump(mode="json"))
# Quick typed acknowledgement to stop retries return AccountBalanceTimeoutCallbackResponse() # defaults to ResultCode=0, ResultDesc="Timeout notification received and processed successfully."Important behaviors & validations
- IdentifierType must be one of the supported enum values (1=MSISDN, 2=Till number, 4=Short code). Invalid values should be rejected before sending requests.
- Remarks must not exceed 100 characters; the client/model will raise validation errors if this rule is violated.
- The initial /query call returns an acknowledgement (ResponseCode/ResponseDescription). This does not contain the final balance.
- Final account balances are delivered asynchronously to your ResultURL (ResultType/ResultCode = 0 indicates success).
Response helpers
- A returned AccountBalanceResponse model exposes:
- is_successful helper that treats any all-zero ResponseCode string (e.g., "0" or "000") as success.
- Result callbacks, once parsed via
process_account_balance_callback, are typedAccountBalanceResultCallbackobjects with: - ResultParameter.ResultParameters list containing an 'AccountBalance' entry holding a delimited balance string.
process_account_balance_callback and process_account_balance_timeout are identical on MpesaClient and AsyncMpesaClient — they're plain validation/parsing methods with no network I/O, so there's nothing to await, even inside an async webhook handler.
Testing & Expected Behaviors
-
Query:
- Should return an AccountBalanceResponse acknowledgement. Confirm
is_successfulforResponseCode == 0. True whether called viaclient.balance.query(...)orawait client.balance.query(...). - HTTP client must receive the correct path and headers (
Authorization Bearer token + JSON content-type).
- Should return an AccountBalanceResponse acknowledgement. Confirm
-
Result Callback:
- Use
process_account_balance_callbackto parse Result.ResultParameter.ResultParameters and extract the AccountBalance key. Persist parsed balances for reconciliation. - Ensure your handler returns the expected acknowledgement payload quickly so provider retries are avoided.
- Use
-
Timeout Callback:
- QueueTimeOutURL receives a Result with ResultType=1; parse it with
process_account_balance_timeoutand handle accordingly (mark request as timed out, alert operations).
- QueueTimeOutURL receives a Result with ResultType=1; parse it with
Next Steps
- Implement secure callback endpoints (IP restrictions, TLS).
- Persist both acknowledgements and final result notifications for audit and reconciliation.
- Add observability & alerting for timeout notifications and unexpected result codes.
Related Documentation
- 📡 Webhook Setup Guide - Best practices for building reliable endpoints
- 🏗️ Production Setup - Go-live checklist, security and monitoring