Business Buy Goods
Make business-to-business payments to Buy Goods accounts using M-Pesa by initiating payment requests and handling asynchronous result notifications.
User Stories
- As a fintech product owner, I want to programmatically initiate B2B payments so that suppliers receive funds immediately after approval.
- 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 security credential of the initiator (base64 encoded). |
Amountrequired int | Integer | Transaction amount to be transferred between accounts. |
PartyArequired int | Integer | Shortcode from which money is deducted for the payment. |
PartyBrequired int | Integer | Shortcode to which money is credited (Buy Goods till number). |
AccountReferencerequired str | String | Account number associated with the payment. |
Requester str | String | Consumer's mobile number (optional). |
Remarksrequired str | String | Additional transaction information (maximum 100 characters). |
QueueTimeOutURLrequired str | String | HTTPS endpoint that will receive timeout notifications. |
ResultURLrequired str | String | HTTPS endpoint that will receive result notifications. |
Occassion str | String | Additional transaction information (optional). |
CommandID str | String | Command ID for the transaction (default: 'BusinessBuyGoods'). |
SenderIdentifierType int | Integer | Identifier type for sender (default: 4 for shortcode). |
RecieverIdentifierType int | Integer | Identifier type for receiver (default: 4 for shortcode). |
Response Schema
BusinessBuyGoodsResponse — the synchronous acknowledgement returned by client.b2b.buygoods(...)
| Parameter | Type | Description |
|---|---|---|
OriginatorConversationIDrequired str | null | String | Unique ID for the request message. |
ConversationIDrequired str | null | String | Unique ID for the transaction. |
ResponseCoderequired str | String | Status code of the request. 0 means success. |
ResponseDescriptionrequired str | String | Status message describing the request outcome. |
Overview
Business Buy Goods enables businesses to make payments to Buy Goods 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 (
BusinessBuyGoodsservice +TokenManager+HttpClient) if you need full control over request/response handling, middleware, or custom error behaviors. - Both are available as sync and async variants (
MpesaClient/AsyncMpesaClient,BusinessBuyGoods/AsyncBusinessBuyGoods) — reach for async when calling from inside an async web framework, or when sending several Buy Goods payments concurrently.
The facade handles token retrieval and attaches Authorization headers so you can call high-level operations like initiating Business Buy Goods payments with minimal boilerplate.
Quick Setup (Sync)
# Example: initiate Business Buy Goods payment using the high-level clientfrom mpesakit import MpesaClient
client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
resp = client.b2b.buygoods( initiator="API_Username", security_credential="encrypted_credential", amount=239, party_a=123456, party_b=654321, account_reference="353353", requester="254700000000", remarks="Payment for goods", result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout", occassion="Purchase")
if resp.is_successful: print("Business Buy Goods payment initiated successfully")else: print("Business Buy Goods payment failed:", resp.ResponseDescription)- The facade returns typed Pydantic models (e.g.,
BusinessBuyGoodsResponse) 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.b2b.buygoods( initiator="API_Username", security_credential="encrypted_credential", amount=239, party_a=123456, party_b=654321, account_reference="353353", requester="254700000000", remarks="Payment for goods", result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout", occassion="Purchase" )
if resp.is_successful: print("Business Buy Goods payment initiated successfully") else: print("Business Buy Goods payment failed:", resp.ResponseDescription)
asyncio.run(main())In a long-lived service (e.g. AsyncMpesaClient wired up 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)
The callback is an inbound HTTP POST from Safaricom to your ResultURL/QueueTimeOutURL — it isn't tied to whether the original request was sent via the sync or async client. The FastAPI handler below stays framework-async regardless, and validates the payload directly against the SDK's response schemas.
# Example: simple FastAPI endpoints for Business Buy Goods Result and Timeoutfrom fastapi import FastAPI, Request, HTTPExceptionfrom mpesakit.business_buy_goods import BusinessBuyGoodsResultCallback, BusinessBuyGoodsResultCallbackResponse, BusinessBuyGoodsTimeoutCallback, BusinessBuyGoodsTimeoutCallbackResponsefrom mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
app = FastAPI()
@app.post("/b2b/buygoods/result")async def buygoods_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 = BusinessBuyGoodsResultCallback(**payload) # will validate incoming fields # process the result (update database, notify user, etc.) ack = BusinessBuyGoodsResultCallbackResponse() return ack.model_dump(mode="json")
@app.post("/b2b/buygoods/timeout")async def buygoods_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 = BusinessBuyGoodsTimeoutCallback(**payload) # will validate incoming fields # process timeout notification (log, retry logic, etc.) ack = BusinessBuyGoodsTimeoutCallbackResponse() return ack.model_dump(mode="json")- Result and timeout endpoints should return acknowledgements (ResultCode 0). This confirms receipt to Safaricom.
- Process the actual Business Buy Goods result asynchronously based on the data in the result callback.
Result Callback Schema
BusinessBuyGoodsResultCallback — posted to ResultURL once the payment completes
| Parameter | Type | Description |
|---|---|---|
Result.ResultTyperequired int | Integer | 0 = success, 1 = failure. |
Result.ResultCoderequired int | str | Integer/String | 0 indicates the payment 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 request. |
Result.ConversationIDrequired str | String | Matches the ConversationID from the initial acknowledgement. |
Result.TransactionID str | null | String | M-Pesa transaction ID for the payment, when successful. |
Result.ResultParameters.ResultParameter list[{Key, Value}] | {Key, Value} | Array/Object | Transaction details, e.g. DebitAccountBalance, Amount, DebitPartyAffectedAccountBalance, TransCompletedTime, DebitPartyCharges, ReceiverPartyPublicName, Currency, InitiatorAccountCurrentBalance. |
Result.ReferenceData.ReferenceItem list[{Key, Value}] | {Key, Value} | Array/Object | Reference items, e.g. BillReferenceNumber, QueueTimeoutURL. |
Timeout Callback Schema
BusinessBuyGoodsTimeoutCallback — posted to QueueTimeOutURL if the payment doesn't complete in time
Uses the same Result shape as the Result Callback Schema above (it's the same BusinessBuyGoodsResultMetadata model), just delivered to QueueTimeOutURL instead of ResultURL, typically with a non-zero ResultCode.
Callback Acknowledgement Schemas
What your webhook handler should return to Safaricom
| Parameter | Type | Description |
|---|---|---|
ResultCode int | str | Integer/String | Defaults to 0. Used by BusinessBuyGoodsResultCallbackResponse and BusinessBuyGoodsTimeoutCallbackResponse — the typed models to return from your /result and /timeout handlers respectively. |
ResultDesc str | String | Defaults to 'Callback received successfully.' (result) or 'Timeout notification received successfully.' (timeout). |
Responses & Helpers
{ "OriginatorConversationID": "5118-111210482-1", "ConversationID": "AG_20230420_2010759fd5662ef6d054", "ResponseCode": "0", "ResponseDescription": "Accept the service request successfully."}- BusinessBuyGoodsResponse provides is_successful which normalizes success checks. Available identically on responses from 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.b2b.buygoods(...)except Exception as exc: # The underlying HTTP client may raise exceptions on network errors; log and retry as appropriate print("Business Buy Goods payment failed:", exc)- HTTP or network errors raised by the HttpClient bubble up; wrap calls in try/except (sync) or 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
-
Business Buy Goods:
- The service posts to
/mpesa/b2b/v1/paymentrequestwith Authorization header set via TokenManager (or AsyncTokenManager). - Responses are returned as
BusinessBuyGoodsResponseinstances, 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
BusinessBuyGoodsResultCallbackandBusinessBuyGoodsTimeoutCallback. 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 Business Buy Goods 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