Tax Remittance
Remit taxes to KRA using M-Pesa by initiating tax payment requests and handling asynchronous result notifications.
User Stories
- As a fintech product owner, I want to programmatically remit taxes to KRA so that I can automate compliance processes.
- 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 | Username used to initiate the request (must be pre-approved by Safaricom). |
SecurityCredentialrequired str | String | Encrypted security credential of the initiator (base64 encoded). |
Amountrequired int | Integer | Transaction amount in KES to be remitted to KRA. |
PartyArequired int | Integer | Shortcode from which money is deducted for tax remittance. |
AccountReferencerequired str | String | Payment Registration Number (PRN) issued by KRA for the tax payment. |
Remarksrequired str | String | Additional information for the transaction (maximum 100 characters). |
QueueTimeOutURLrequired str | String | HTTPS endpoint that will receive timeout notifications. |
ResultURLrequired str | String | HTTPS endpoint that will receive result notifications. |
PartyB int | Integer | KRA shortcode (default: 572572). |
CommandID str | String | Command ID for the transaction (default: 'PayTaxToKRA'). |
SenderIdentifierType int | Integer | Identifier type for sender (default: 4 for shortcode). |
RecieverIdentifierType int | Integer | Identifier type for receiver (default: 4 for shortcode). |
Response Schema
TaxRemittanceResponse — the synchronous acknowledgement returned by client.tax.remittance(...)
| Parameter | Type | Description |
|---|---|---|
OriginatorConversationIDrequired str | null | String | Unique ID for the request message. |
ConversationIDrequired str | null | String | Unique ID for the transaction. |
ResponseCoderequired str | int | String/Integer | Status code of the request. 0 means success. |
ResponseDescriptionrequired str | String | Status message describing the request outcome. |
Overview
Tax Remittance enables businesses to pay taxes directly to KRA through M-Pesa. 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 (
TaxRemittanceservice +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,TaxRemittance/AsyncTaxRemittance) — reach for async when remitting taxes from inside an async web framework, or when filing several PRNs concurrently at the end of a reporting period.
The facade handles token retrieval and attaches Authorization headers so you can call high-level operations like initiating tax remittance with minimal boilerplate.
Quick Setup (Sync)
# Example: initiate tax remittance using the high-level clientfrom mpesakit import MpesaClient
client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
resp = client.tax.remittance( initiator="TaxPayer", security_credential="encrypted_credential", amount=239, party_a=888880, account_reference="353353", remarks="Tax payment for Q1", result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout")
if resp.is_successful: print("Tax remittance initiated successfully")else: print("Tax remittance failed:", resp.ResponseDescription)- The facade returns typed Pydantic models (e.g., TaxRemittanceResponse) 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 AsyncMpesaClient
async def main(): async with AsyncMpesaClient( consumer_key="...", consumer_secret="...", environment="sandbox" ) as client: resp = await client.tax.remittance( initiator="TaxPayer", security_credential="encrypted_credential", amount=239, party_a=888880, account_reference="353353", remarks="Tax payment for Q1", result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout" )
if resp.is_successful: print("Tax remittance initiated successfully") else: print("Tax remittance failed:", resp.ResponseDescription)
asyncio.run(main())If you're remitting taxes for multiple Payment Registration Numbers in one run, gather the coroutines rather than awaiting sequentially — but check Safaricom's rate limits and chunk large batches:
prns = [
dict(account_reference="PRN-001", amount=15000),
dict(account_reference="PRN-002", amount=8200),
# ...
]
responses = await asyncio.gather(*(
client.tax.remittance(
initiator="TaxPayer",
security_credential="encrypted_credential",
amount=p["amount"],
party_a=888880,
account_reference=p["account_reference"],
remarks="Tax payment for Q1",
result_url="https://your.example/result",
queue_timeout_url="https://your.example/timeout",
)
for p in prns
))
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/Daraja — independent of whether the remittance was filed via MpesaClient or AsyncMpesaClient. For the result payload, both clients expose process_tax_remittance_callback, a one-call shorthand for TaxRemittanceResultCallback.model_validate(payload); it's plain validation with no I/O, so it's never awaited even on the async client. There's no equivalent helper for the timeout payload, so that one is still validated directly against TaxRemittanceTimeoutCallback.
# Example: simple FastAPI endpoints for Tax Remittance Result and Timeoutfrom fastapi import FastAPI, Request, HTTPExceptionfrom mpesakit import MpesaClient # or AsyncMpesaClient — process_tax_remittance_callback is identical on bothfrom mpesakit.tax_remittance import TaxRemittanceResultCallbackResponse, TaxRemittanceTimeoutCallback, TaxRemittanceTimeoutCallbackResponsefrom mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
app = FastAPI()client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
@app.post("/tax/result")async def tax_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")
# Equivalent to TaxRemittanceResultCallback.model_validate(payload) data = client.process_tax_remittance_callback(payload) # process the result (update database, notify user, etc.) ack = TaxRemittanceResultCallbackResponse() return ack.model_dump(mode="json")
@app.post("/tax/timeout")async def tax_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 = TaxRemittanceTimeoutCallback(**payload) # will validate incoming fields # process timeout notification (log, retry logic, etc.) ack = TaxRemittanceTimeoutCallbackResponse() return ack.model_dump(mode="json")- Result and timeout endpoints should return acknowledgements (ResultCode 0). This confirms receipt to Mpesa Daraja API.
- Process the actual tax remittance result asynchronously based on the data in the result callback.
Result Callback Schema
TaxRemittanceResultCallback — posted to ResultURL once the remittance completes
| Parameter | Type | Description |
|---|---|---|
Result.ResultTyperequired int | Integer | 0 = success, 1 = failure. |
Result.ResultCoderequired int | str | Integer/String | 0 indicates the remittance 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 remittance, when successful. |
Result.ResultParameters.ResultParameter list[{Key, Value}] | Array | Transaction details, e.g. Amount, Currency, TransCompletedTime. |
Result.ReferenceData.ReferenceItem list[{Key, Value}] | Array | Reference items, e.g. BillReferenceNumber, QueueTimeoutURL. |
Timeout Callback Schema
TaxRemittanceTimeoutCallback — posted to QueueTimeOutURL if the remittance doesn't complete in time
Uses the same Result shape as the Result Callback Schema above (it's the same TaxRemittanceResultMetadata model), just delivered to QueueTimeOutURL instead of ResultURL, typically with a non-zero ResultCode and without TransactionID/ResultParameters.
Callback Acknowledgement Schemas
What your webhook handler should return to Safaricom
| Parameter | Type | Description |
|---|---|---|
ResultCode int | str | Integer/String | Defaults to 0. Used by TaxRemittanceResultCallbackResponse and TaxRemittanceTimeoutCallbackResponse — 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 and processed successfully.' (timeout). |
Responses & Helpers
{ "OriginatorConversationID": "5118-111210482-1", "ConversationID": "AG_20230420_2010759fd5662ef6d054", "ResponseCode": "0", "ResponseDescription": "Accept the service request successfully."}- TaxRemittanceResponse 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.
Error Handling
# Handle errors when calling the servicetry: resp = client.tax.remittance(...)except Exception as exc: # The underlying HTTP client may raise exceptions on network errors; log and retry as appropriate print("Tax remittance 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
-
Tax Remittance:
- The service posts to
/mpesa/b2bpayment/v1/remittaxwithAuthorizationheader set viaTokenManager(orAsyncTokenManager). - Responses are returned as
TaxRemittanceResponseinstances, 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
TaxRemittanceResultCallback(directly, or viaprocess_tax_remittance_callback) andTaxRemittanceTimeoutCallback. 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 tax remittance 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