Customer to Business (C2B)
Manage Customer-to-Business (C2B) flows: register Validation and Confirmation endpoints with Safaricom, validate incoming payments, and send confirmations/acknowledgements.
NB: C2B Transaction Validation is an optional feature that needs to be activated on M-Pesa. The owner of the shortcode must request activation by emailing apisupport@safaricom.co.ke or M-pesabusiness@safaricom.co.ke if they need their transactions validated before execution.
User Stories
- As a fintech product owner, I want to programmatically manage C2B payments so that customers 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 |
|---|---|---|
ShortCoderequired int | Integer | Organization's PayBill or Till shortcode to register URLs for. |
ResponseTyperequired str | String | Default behavior if ValidationURL cannot be reached. Allowed: 'Completed' or 'Cancelled'. |
ConfirmationURLrequired str | String | HTTPS endpoint that will receive payment confirmation notifications. |
ValidationURLrequired str | String | HTTPS endpoint that will receive validation callbacks before accepting payments. |
URL Registration Response Schema
C2BRegisterUrlResponse — the acknowledgement returned by client.c2b.register_url(...)
| Parameter | Type | Description |
|---|---|---|
OriginatorConversationIDrequired str | null | String | Unique ID for the registration request. |
ResponseCoderequired str | int | String/Integer | Status code. Any all-zero value (e.g. '0') indicates success. |
ResponseDescriptionrequired str | String | Status message, e.g. 'success'. |
Validation & Confirmation Callback Schema
C2BValidationRequest — the payment payload posted to both ValidationURL and ConfirmationURL
Safaricom posts the same payload shape to both URLs: a validation request before the payment is accepted (only if external validation is enabled on your shortcode), and a confirmation notice once the payment has completed.
| Parameter | Type | Description |
|---|---|---|
TransactionTyperequired str | String | Type of transaction (e.g. 'Pay Bill', 'Buy Goods'). |
TransIDrequired str | String | Unique M-Pesa transaction identifier. |
TransTimerequired str | String | Timestamp of transaction in YYYYMMDDHHmmss format. |
TransAmountrequired float | Float | Amount transacted (whole numbers expected by M-Pesa). |
BusinessShortCoderequired int | Integer | Receiving organization's shortcode. |
BillRefNumber str | null | String | Account/reference number supplied by payer (PayBill only, max 20 chars). |
InvoiceNumber str | null | String | Invoice number, when applicable. |
OrgAccountBalance str | null | String | Organization account balance after the payment. |
ThirdPartyTransID str | null | String | Partner transaction ID, when applicable. |
MSISDNrequired int | str | Integer/String | Customer mobile number making the payment. |
FirstName str | null | String | Customer's first name, when known to M-Pesa. |
MiddleName str | null | String | Customer's middle name, when known to M-Pesa. |
LastName str | null | String | Customer's last name, when known to M-Pesa. |
Validation Response Schema
C2BValidationResponse — what your /validation handler should return
| Parameter | Type | Description |
|---|---|---|
ResultCoderequired str | int | String/Integer | '0' to accept the payment, or one of the C2B validation error codes to reject it (see Validation Result Codes below). |
ResultDescrequired str | String | Short description, e.g. 'Accepted' or 'Rejected' (<= 90 chars recommended). |
ThirdPartyTransID str | null | String | Optional partner transaction id to echo back. |
Confirmation Acknowledgement Schema
C2BConfirmationResponse — what your /confirmation handler should return
| Parameter | Type | Description |
|---|---|---|
ResultCode int | str | Integer/String | Defaults to 0 (success). |
ResultDesc str | String | Defaults to 'Success'. |
Overview
C2B (Customer-to-Business) covers URL registration with Safaricom (so their platform can call your services), validating incoming payments and acknowledging confirmations.
- Use the MpesaClient facade for simple and safe integration: it manages authentication, header injection and returns typed Pydantic models.
- Use the Direct API (C2B service + 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,C2B/AsyncC2B) — reach for async when registering or reconciling from inside an async web framework, or when registering URLs for multiple shortcodes concurrently.
The facade handles token retrieval and attaches Authorization headers so you can call high-level operations like registering C2B URLs with minimal boilerplate.
Quick Setup (Sync)
# Example: register C2B URLs using the high-level clientfrom mpesakit import MpesaClientfrom mpesakit.c2b import C2BResponseType
client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
resp = client.c2b.register_url( short_code=600999, response_type=C2BResponseType.COMPLETED, confirmation_url="https://your.example/confirmation", validation_url="https://your.example/validation",)
if resp.is_successful: print("Registration accepted")else: print("Registration failed:", resp.ResponseDescription)- The facade returns typed Pydantic models (e.g., C2BRegisterUrlResponse) for ergonomic access to fields and helpers like is_successful.
- Authentication tokens are handled transparently by the client.
Quick Setup (Async)
import asynciofrom mpesakit import AsyncMpesaClientfrom mpesakit.c2b import C2BResponseType
async def main(): async with AsyncMpesaClient( consumer_key="...", consumer_secret="...", environment="sandbox" ) as client: resp = await client.c2b.register_url( short_code=600999, response_type=C2BResponseType.COMPLETED, confirmation_url="https://your.example/confirmation", validation_url="https://your.example/validation", )
if resp.is_successful: print("Registration accepted") else: print("Registration failed:", resp.ResponseDescription)
asyncio.run(main())If you manage several PayBill/Till shortcodes, register their URLs concurrently instead of one at a time:
short_codes = [600999, 600123, 600456]
responses = await asyncio.gather(*(
client.c2b.register_url(
short_code=sc,
response_type=C2BResponseType.COMPLETED,
confirmation_url="https://your.example/confirmation",
validation_url="https://your.example/validation",
)
for sc in short_codes
))
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 (Validation & Confirmation)
Validation and Confirmation callbacks are inbound HTTP POSTs from Safaricom — independent of whether the URLs were registered via the sync or async client. Handlers stay framework-async either way (FastAPI shown below), and validate the payload directly against the SDK's request/response schemas.
# Example: simple FastAPI endpoints for Validation and Confirmationfrom fastapi import FastAPI, Request, HTTPExceptionfrom mpesakit.c2b import C2BValidationRequest, C2BValidationResponse, C2BConfirmationResponsefrom mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
app = FastAPI()
@app.post("/c2b/validation")async def validation(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 = C2BValidationRequest(**payload) # will validate incoming fields # perform business checks (account exists, limits, etc.) result = C2BValidationResponse(ResultCode="0", ResultDesc="Accepted", ThirdPartyTransID=data.ThirdPartyTransID) return result.model_dump(mode="json")
@app.post("/c2b/confirmation")async def confirmation(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")
# process final payment notification (store transaction, update balance, etc.) ack = C2BConfirmationResponse() # ResultCode=0, ResultDesc="Success" return ack.model_dump(mode="json")- Validation endpoints should return a C2BValidationResponse. Use ResultCode="0" to accept or one of the defined error codes (see Validation Result Codes) to reject.
- Confirmation endpoints should return an acknowledgement (ResultCode 0). This confirms receipt to Safaricom.
Validation Result Codes
{ "0": "Accepted", "C2B00011": "Invalid MSISDN", "C2B00012": "Invalid Account Number", "C2B00013": "Invalid Amount", "C2B00014": "Invalid KYC Details", "C2B00015": "Invalid Shortcode", "C2B00016": "Other Error"}- Keep ResultDesc concise. The library warns if ResultDesc exceeds 90 characters.
Responses & Helpers
{ "OriginatorConversationID": "7619-37765134-1", "ResponseCode": "0", "ResponseDescription": "success"}- C2BRegisterUrlResponse provides is_successful which treats any all-zero string (e.g., "0" or "00000000") as success. 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.
Validation & Safety Checks
- When registering URLs avoid embedding sensitive or provider-related keywords such as 'm-pesa', 'mpesa', 'safaricom' or suspicious file/command keywords (exe, cmd, sql, query). The library warns about such keywords because Safaricom's API may reject them with a 400 error.
Error Handling
# Handle errors when calling the servicetry: resp = client.c2b.register_url(...)except Exception as exc: # The underlying HTTP client may raise exceptions on network errors; log and retry as appropriate print("Registration 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
-
Register URL:
- The service posts to /mpesa/c2b/v1/registerurl with Authorization header set via TokenManager (or AsyncTokenManager).
- Responses are returned as C2BRegisterUrlResponse instances, 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.
-
Validation:
- Incoming payloads are validated against C2BValidationRequest. Missing required fields or invalid formats will raise validation errors.
- Use the provided enums for allowed ResultCode values; invalid codes are rejected by the model validator.
-
Confirmation:
- Return a C2BConfirmationResponse (ResultCode 0 and ResultDesc "Success") to acknowledge receipt.
Next Steps
- Implement robust webhook handlers for validation and confirmation. Log and persist notifications to support reconciliation.
- Add observability and retry strategies around register_url 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