Paybill and Till (C2B)
By the end of this track you can charge a phone and confirm the result.
0/4 finished
Step 1
The idea
Here the customer starts the payment from their phone menu. You register a confirmation URL (and optionally a validation URL) for your shortcode, and M-Pesa calls it on each payment. The customer types an account number, and you match it to an invoice.
Step 2
How it works
How it differs from STK Push
With C2B, the customer starts the payment from the M-Pesa menu: Lipa na M-Pesa, Paybill or Buy Goods, your number, an account number, the amount, their PIN. You don't send a request; you wait to be told.
1. Register your URLs
POST to /mpesa/c2b/v2/registerurl to tell Daraja where to send payments for your shortcode. The confirmation URL receives every completed payment. The validation URL can accept or reject a payment before it completes, but it is only called if Safaricom enables external validation on your shortcode; it is off by default. ResponseType says what happens if your validation URL fails or times out: Completed accepts the payment, Cancelled rejects it.
{ "ShortCode": "600984", "ResponseType": "Completed", "ConfirmationURL": "https://your-domain.example/c2b/confirm", "ValidationURL": "https://your-domain.example/c2b/validate"}2. Receive a confirmation
Each payment arrives as a POST like this. TransID is the M-Pesa receipt; store it with a unique constraint. BillRefNumber is what the customer typed as the account number (Till payments have none). TransAmount is a string, and v2 masks the phone number.
{ "TransactionType": "Pay Bill", "TransID": "TJT4XXXXXX", "TransTime": "20261029143045", "TransAmount": "500.00", "BusinessShortCode": "600984", "BillRefNumber": "INV-1042", "InvoiceNumber": "", "OrgAccountBalance": "25500.00", "ThirdPartyTransID": "", "MSISDN": "2547 ***** 126", "FirstName": "JANE", "MiddleName": "", "LastName": ""}3. Match it to an invoice
Look up BillRefNumber in your invoices, ignoring case and spaces, because people type it by hand. If nothing matches, or the amount is short, store the payment as unmatched for a person to resolve. Reply with {"ResultCode": "0", "ResultDesc": "Success"} either way: the money has already moved, and your reply can't undo it.
4. Test it
In the sandbox, /mpesa/c2b/v2/simulate pretends to be a customer paying your test shortcode. Register your URLs first. Simulate doesn't exist in production; there you pay from a real phone.
{ "ShortCode": 600984, "CommandID": "CustomerPayBillOnline", "Amount": 1, "Msisdn": 254708374149, "BillRefNumber": "INV-1042"}5. Catch what you missed
Confirmations get lost. In production, the Pull Transactions API returns your shortcode's payments for a time window. Run it on a schedule and insert by TransID, so anything you already have is skipped.
Step 3
Try it
Register your URLs with mpesa_c2b_register_urls, then send a test payment with mpesa_c2b_simulate (sandbox only).
Step 4
Common mistakes
- Callback URLs containing
mpesa,m-pesa,safaricom,exec,cmd,sqlorquery. Daraja refuses to register them. - Registering URLs casually in production. It is effectively one-time: a second try returns
Urls are already registered, and changing them needs Safaricom. - Expecting validation calls without asking Safaricom to enable them.
- Dropping a payment because the account number is wrong. The customer has paid; keep it.
Go deeper
A payment arrives with an account number you don't know. What do you do?
Answer in your own words first.
Show the answerHide the answer
Store it as unmatched and resolve it. Never drop it.