daraja-c2b · diff
git:20261019.15b8109 to git:20261019.9e3c915
60 added, 41 removed. Audit A to A.
---
name: daraja-c2b
- description: Implement Safaricom Daraja C2B (Customer-to-Business) — registering Validation and Confirmation URLs for a Paybill or Till, handling customer-initiated payments, validating account numbers, and simulating C2B in sandbox. Use when customers pay via the M-Pesa menu/app to a Paybill or Buy Goods till and the system must record them.
+ description: Implements Safaricom Daraja C2B (Customer-to-Business) for Paybill and Till numbers. Covers registering Validation and Confirmation URLs, accepting or rejecting payments with C2B00011-C2B00016 codes, recording confirmations idempotently, simulating in sandbox, and recovering missed payments with the Pull Transactions API. Use when customers pay from the M-Pesa menu or app and the system must record or validate those payments.
---
- # C2B — Customer to Business (Paybill / Till)
+ # C2B: Customer to Business
- Customer initiates the payment from their phone (Lipa na M-Pesa → Paybill / Buy Goods). Your system only *receives* notifications, so you must first **register URLs**.
+ The customer starts the payment from their phone (Lipa na M-Pesa → Paybill / Buy Goods). Your system only **receives** notifications, so the URLs must be registered first.
## 1. Register URLs
- `POST /mpesa/c2b/v2/registerurl` (v1 also exists; v2 masks the MSISDN)
+ `POST /mpesa/c2b/v2/registerurl`. v2 masks the customer's MSISDN; v1 is the older endpoint.
+
```json
- {
- "ShortCode": "600984",
- "ResponseType": "Completed",
- "ConfirmationURL": "https://example.com/payments/c2b/confirmation",
- "ValidationURL": "https://example.com/payments/c2b/validation"
- }
+ {"ShortCode": "600984", "ResponseType": "Completed",
+ "ConfirmationURL": "https://example.com/payments/c2b/confirm",
+ "ValidationURL": "https://example.com/payments/c2b/validate"}
```
- - `ResponseType` = what M-Pesa does if your ValidationURL is unreachable/times out: `Completed` (accept) or `Cancelled` (reject).
- - External validation is **off by default** in production; request Safaricom to enable it if you need to reject payments.
- - Sandbox lets you re-register freely; in production registration is usually one-time — changes go through Safaricom support.
- - URL rules: HTTPS, publicly reachable, no "mpesa", "safaricom", "m-pesa", "exec", "cmd", "sql" etc. in the URL, no IP-based or ngrok-style URLs in production.
- ## 2. Validation request (optional, before debit)
+ - `ResponseType` is what M-Pesa does if your ValidationURL fails or times out: `Completed` accepts the payment, `Cancelled` rejects it.
+ - The **ValidationURL is only called if External Validation is enabled** on the shortcode. It is off by default; ask Safaricom (apisupport@safaricom.co.ke) to enable it.
+ - **Sandbox**: re-register as often as you like. **Production**: registration is effectively one-time. A second attempt returns `500.003.1001 "Urls are already registered"`, and changing them needs Safaricom to delete the old ones.
+ - The URLs must be HTTPS and public, must not contain `mpesa`, `m-pesa`, `safaricom`, `exec`, `cmd`, `sql` or `query`, and must not be tunnel URLs in production.
- Daraja POSTs:
+ Validate first and repeat until it prints `OK`: `python ../mpesa-daraja/scripts/validate_request.py c2b_register body.json`
+
+ ## 2. Validation (optional, before the debit)
+
+ Respond within a few seconds:
+
```json
- {"TransactionType":"Pay Bill","TransID":"RKTQDM7W6S","TransTime":"20261019153045",
- "TransAmount":"10.00","BusinessShortCode":"600984","BillRefNumber":"ACC123",
- "InvoiceNumber":"","OrgAccountBalance":"","ThirdPartyTransID":"",
- "MSISDN":"2547 ***** 126","FirstName":"JOHN","MiddleName":"","LastName":""}
+ {"ResultCode": "0", "ResultDesc": "Accepted"}
```
- Respond within a few seconds:
+
+ or reject:
+
```json
- {"ResultCode":"0","ResultDesc":"Accepted"}
+ {"ResultCode": "C2B00012", "ResultDesc": "Rejected"}
```
- or reject with a code, e.g. `{"ResultCode":"C2B00012","ResultDesc":"Rejected"}`:
- | Code | Meaning |
+ | Code | Reject reason |
|---|---|
| C2B00011 | Invalid MSISDN |
- | C2B00012 | Invalid Account Number |
- | C2B00013 | Invalid Amount |
+ | C2B00012 | Invalid account number |
+ | C2B00013 | Invalid amount |
| C2B00014 | Invalid KYC details |
- | C2B00015 | Invalid Short code |
+ | C2B00015 | Invalid shortcode |
| C2B00016 | Other error |
- ## 3. Confirmation (after completion)
+ You may also return `ThirdPartyTransID` on an accept; it is echoed back on the confirmation.
- Same payload shape, sent when money has moved. Persist it idempotently keyed on `TransID`, then reply:
+ ## 3. Confirmation (money has moved)
+
```json
- {"ResultCode":"0","ResultDesc":"Success"}
+ {"TransactionType": "Pay Bill", "TransID": "RKL51ZDR4F", "TransTime": "20231121121325",
+ "TransAmount": "5.00", "BusinessShortCode": "600966", "BillRefNumber": "ACC123",
+ "InvoiceNumber": "", "OrgAccountBalance": "25.00", "ThirdPartyTransID": "",
+ "MSISDN": "2547 ***** 126", "FirstName": "NICHOLAS", "MiddleName": "", "LastName": ""}
```
- Your reply does **not** reverse anything — the money is already received. Match `BillRefNumber` to the customer account / invoice; route unmatched payments to a manual-review queue.
+ - Upsert on a unique `TransID`.
+ - Match `BillRefNumber` to the customer or invoice; send unmatched payments to a manual-review queue.
+ - Reply `{"ResultCode": "0", "ResultDesc": "Success"}`. Your reply cannot undo the payment.
+ - `TransAmount` is a string, and the MSISDN is masked in v2. Till payments have no `BillRefNumber`.
+
## 4. Simulate (sandbox only)
- `POST /mpesa/c2b/v1/simulate`
+ `POST /mpesa/c2b/v2/simulate`
+
```json
- {"ShortCode":"600984","CommandID":"CustomerPayBillOnline","Amount":10,
- "Msisdn":"254708374149","BillRefNumber":"ACC123"}
+ {"ShortCode": 600984, "CommandID": "CustomerPayBillOnline", "Amount": 1,
+ "Msisdn": 254708374149, "BillRefNumber": "ACC123"}
```
- For a Till use `CommandID: "CustomerBuyGoodsOnline"` and omit/blank `BillRefNumber`.
- ## Pitfalls
+ For a Till, use `CommandID: "CustomerBuyGoodsOnline"` and set `BillRefNumber` to null. Register URLs before simulating.
- - Forgetting to register URLs (or registering on the wrong shortcode) → no callbacks ever.
- - Slow validation handler → M-Pesa applies `ResponseType` default.
- - Till payments have no account number; identify by amount + phone + time or use STK Push instead.
- - Treating `TransAmount` as a number without parsing the string.
- - Not reconciling against the M-Pesa statement / Account Balance periodically.
+ ## 5. Recover missed confirmations: Pull Transactions
+
+ Callbacks get lost. The Pull API lets you fetch transactions for a time window. It is production only, and the shortcode is registered once:
+
+ | Step | Endpoint | Body |
+ |---|---|---|
+ | Register (once) | `POST /pulltransactions/v1/register` | `{"ShortCode":"600000","RequestType":"Pull","NominatedNumber":"0722000000","CallBackURL":"https://…"}` |
+ | Query | `POST /pulltransactions/v1/query` | `{"ShortCode":"600000","StartDate":"2026-10-19 08:00:00","EndDate":"2026-10-19 10:00:00","OffSetValue":"0"}` |
+
+ - Page through results by increasing `OffSetValue`.
+ - Errors: `400.001` invalid shortcode, `401.001` bad token, `500` no transactions in the window.
+ - Run Pull on a schedule and upsert by `TransID`. Daraja's 5-attempt retries plus Pull give near-complete coverage.
+
+ ## MCP
+
+ `mpesa:mpesa_c2b_register_urls`, `mpesa:mpesa_c2b_simulate` (sandbox only), `mpesa:mpesa_pull_transactions`, and `mpesa:mpesa_parse_callback` (for confirmation payloads).