B2C Account TopUp
Top up business accounts using M-Pesa by initiating account top-up requests and handling asynchronous result notifications.
User Stories
- As a fintech product owner, I want to programmatically top up business accounts so that merchants receive funds immediately after customer payments.
- As an integrations developer, I want a simple client and clear webhook callbacks so I can implement reliable end-to-end flows with minimal boilerplate.
- As a billing operations engineer, I want result and timeout notifications with acknowledgements so I can reconcile transactions and trigger retries or alerts when needed.
- As a reseller partner, I want a tested SDK and examples so I can onboard quickly and reduce integration defects.
Parameters Definition
| Parameter | Type | Description |
|---|---|---|
Initiatorrequired str | String | M-Pesa API operator username (must be pre-approved by Safaricom). |
SecurityCredentialrequired str | String | Encrypted password of the API operator (base64 encoded). |
Amountrequired int | Integer | Transaction amount to be transferred for top-up. |
PartyArequired int | Integer | Shortcode from which money will be deducted for the top-up. |
PartyBrequired int | Integer | Shortcode to which money will be moved for the top-up. |
AccountReferencerequired str | String | Reference for the transaction. |
Requester str | String | Consumer's mobile number on behalf of whom you are paying (optional). |
Remarks str | String | Additional information for the transaction (optional). |
QueueTimeOutURLrequired str | String | HTTPS endpoint that will receive timeout notifications. |
ResultURLrequired str | String | HTTPS endpoint that will receive result notifications. |
CommandID str | String | Command ID for the transaction (default: 'BusinessPayToBulk'). |
SenderIdentifierType int | Integer | Identifier type for sender (default: 4 for shortcode). |
RecieverIdentifierType int | Integer | Identifier type for receiver (default: 4 for shortcode). |
Response Schema
B2CAccountTopUpResponse — the synchronous acknowledgement returned by client.b2c.account_topup(...)
| Parameter | Type | Description |
|---|---|---|
OriginatorConversationIDrequired str | String | Unique request identifier assigned by Daraja. |
ConversationIDrequired str | String | Unique request identifier assigned by M-Pesa. |
ResponseCoderequired str | String | Status code for request submission. 0 indicates success. |
ResponseDescriptionrequired str | String | Descriptive message of the request submission status. |
Overview
B2C Account TopUp enables businesses to top up their accounts. It's an asynchronous operation with callbacks for results and timeouts.
- Use the
MpesaClientfacade for simple and safe integration: it manages authentication, header injection and returns typed Pydantic models. - Use the Direct API (
B2CAccountTopUpservice +TokenManager+HttpClient) if you need full control over request/response handling, middleware, or custom error behaviors. - Both the facade and the direct service have async counterparts (
AsyncMpesaClient,Async B2Cservice) built on an async HTTP client — use them fromasync defroute handlers or whenever you don't want the top-up request blocking your event loop.
The facade handles token retrieval and attaches Authorization headers so you can call high-level operations like initiating B2C Account TopUp with minimal boilerplate.
Quick Setup (Sync)
# Example: initiate B2C Account TopUp using the high-level clientfrom mpesakit import MpesaClient
client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
resp = client.b2c.account_topup( initiator="testapi", security_credential="encrypted_credential", amount=239, party_a=600979, party_b=600000, account_reference="353353", requester="254708374149", remarks="Account top-up", result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout")
if resp.is_successful: print("B2C Account TopUp initiated successfully")else: print("B2C Account TopUp failed:", resp.ResponseDescription)- The facade returns typed
Pydantic models(e.g.,B2CAccountTopUpResponse) for ergonomic access to fields and helpers likeis_successful. - Authentication tokens are handled transparently by the client.
Quick Setup (Async)
import asynciofrom mpesakit import AsyncMpesaClient
async def main(): async with AsyncMpesaClient( consumer_key="...", consumer_secret="...", environment="sandbox" ) as client: resp = await client.b2c.account_topup( initiator="testapi", security_credential="encrypted_credential", amount=239, party_a=600979, party_b=600000, account_reference="353353", requester="254708374149", remarks="Account top-up", result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout" )
if resp.is_successful: print("B2C Account TopUp initiated successfully") else: print("B2C Account TopUp failed:", resp.ResponseDescription)
asyncio.run(main())For a long-lived service (e.g. AsyncMpesaClient as a FastAPI dependency), construct it once at startup and call await client.aclose() on shutdown instead of opening a new async with block per request.
Webhook Handling (Result & Timeout)
Webhook handlers themselves don't need to be async just because you're calling the M-Pesa API asynchronously elsewhere in your app — async def route handlers work the same regardless of which client variant triggered the original request. The example below stays framework-async (FastAPI) while validating the callback payload directly against the response schemas.
# Example: simple FastAPI endpoints for B2C Account TopUp Result and Timeoutfrom fastapi import FastAPI, Request, HTTPExceptionfrom mpesakit.b2c_account_top_up import B2CAccountTopUpCallback, B2CAccountTopUpCallbackResponse, B2CAccountTopUpTimeoutCallback, B2CAccountTopUpTimeoutCallbackResponsefrom mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
app = FastAPI()
@app.post("/b2c/account-topup/result")async def account_topup_result(request: Request): payload = await request.json() caller_ip = (request.headers.get("x-forwarded-for") or request.client.host).split(",")[0].strip() if not is_mpesa_ip_allowed(caller_ip): raise HTTPException(status_code=403, detail="forbidden")
data = B2CAccountTopUpCallback(**payload) # will validate incoming fields # process the result (update database, notify user, etc.) ack = B2CAccountTopUpCallbackResponse() return ack.model_dump(mode="json")
@app.post("/b2c/account-topup/timeout")async def account_topup_timeout(request: Request): payload = await request.json() caller_ip = (request.headers.get("x-forwarded-for") or request.client.host).split(",")[0].strip() if not is_mpesa_ip_allowed(caller_ip): raise HTTPException(status_code=403, detail="forbidden")
data = B2CAccountTopUpTimeoutCallback(**payload) # will validate incoming fields # process timeout notification (log, retry logic, etc.) ack = B2CAccountTopUpTimeoutCallbackResponse() return ack.model_dump(mode="json")- Result and timeout endpoints should return acknowledgements (ResultCode 0). This confirms receipt to Safaricom.
- Process the actual B2C Account TopUp result asynchronously based on the data in the result callback.
- Regardless of whether the original request was sent via
MpesaClientorAsyncMpesaClient, the callback payload and validation shown above are identical — the callback is just an inbound HTTP POST from Safaricom, not part of the client that sent the request.
Result Callback Schema
B2CAccountTopUpCallback — posted to ResultURL once the top-up completes
| Parameter | Type | Description |
|---|---|---|
Result.ResultTyperequired int | Integer | Status code for the transaction sent to the listener. |
Result.ResultCoderequired int | str | Integer/String | Transaction result status code. 0 means success. |
Result.ResultDescrequired str | String | Descriptive message for the transaction result. |
Result.OriginatorConversationIDrequired str | String | Unique request identifier assigned by the API gateway. |
Result.ConversationIDrequired str | String | Unique request identifier assigned by M-Pesa. |
Result.TransactionIDrequired str | String | Unique M-Pesa transaction ID for the payment request. |
Result.ResultParameters.ResultParameter list[{Key, Value}] | Array | Transaction details, e.g. DebitAccountBalance, Amount, DebitPartyAffectedAccountBalance, TransCompletedTime, DebitPartyCharges, ReceiverPartyPublicName, Currency, InitiatorAccountCurrentBalance. |
Result.ReferenceData.ReferenceItem list[{Key, Value}] | Array | Reference items, e.g. BillReferenceNumber, QueueTimeoutURL. |
Timeout Callback Schema
B2CAccountTopUpTimeoutCallback — posted to QueueTimeOutURL if the top-up doesn't complete in time
| Parameter | Type | Description |
|---|---|---|
Result.ResultTyperequired int | Integer | 1 indicates a timeout. |
Result.ResultCoderequired str | String | '1' indicates a timeout. |
Result.ResultDescrequired str | String | Description of the timeout event, e.g. 'The service request timed out.' |
Result.OriginatorConversationIDrequired str | String | Unique request identifier assigned by Daraja. |
Result.ConversationIDrequired str | String | Unique request identifier assigned by M-Pesa. |
Callback Acknowledgement Schemas
What your webhook handler should return to Safaricom
| Parameter | Type | Description |
|---|---|---|
ResultCode int | str | Integer/String | Defaults to 0. Used by B2CAccountTopUpCallbackResponse and B2CAccountTopUpTimeoutCallbackResponse — the typed models to return from your /result and /timeout handlers respectively. |
ResultDesc str | String | Defaults to 'Callback processed successfully' (result) or 'Timeout notification received and processed successfully.' (timeout). |
Responses & Helpers
{ "OriginatorConversationID": "5118-111210482-1", "ConversationID": "AG_20230420_2010759fd5662ef6d054", "ResponseCode": "0", "ResponseDescription": "Accept the service request successfully."}- B2CAccountTopUpResponse provides is_successful which normalizes success checks. This is available identically on the response returned by the sync and async clients.
- The SDK normalizes minor provider response typos (e.g., 'OriginatorCoversationID') so fields are accessible reliably.
Error Handling
# Handle errors when calling the servicetry: resp = client.b2c.account_topup(...)except Exception as exc: # The underlying HTTP client may raise exceptions on network errors; log and retry as appropriate print("B2C Account TopUp failed:", exc)- HTTP or network errors raised by the HttpClient bubble up; wrap calls in try/except (sync) or try/except around the awaited call (async) for robust production behavior.
- Tests exercise error flows to ensure exceptions propagate when the HTTP client fails, for both client variants.
Testing & Expected Behaviors
-
B2C Account TopUp:
- The service posts to
/mpesa/b2c/v1/paymentrequestwith Authorization header set via TokenManager (or AsyncTokenManager). - Responses are returned as
B2CAccountTopUpResponseinstances, whether awaited from the async client or returned directly from the sync client. - The implementation tolerates a common provider typo ("OriginatorCoversationID") and maps it to
OriginatorConversationID.
- The service posts to
-
Validation:
- Incoming payloads are validated against
B2CAccountTopUpCallbackandB2CAccountTopUpTimeoutCallback. Missing required fields or invalid formats will raise validation errors. - Use the provided response schemas for acknowledgements; invalid codes are rejected by the model validator.
- Incoming payloads are validated against
Next Steps
- Implement robust webhook handlers for result and timeout notifications. Log and persist notifications to support reconciliation.
- Add observability and retry strategies around B2C Account TopUp calls and webhook processing to handle transient failures.
Related Documentation
- 📡 Webhook Setup Guide - Best practices for building reliable endpoints
- 🏗️ Production Setup - Go-live checklist, security and monitoring