If you run an ERP for a Kenyan business, M-Pesa is not an "integration option" — it's how a huge share of your receivables actually arrive. I've wired Safaricom's Daraja API into Acumatica for distributors and service firms in Nairobi, and the pattern is settled enough now that I can describe the whole thing: STK Push for collections, a middleware that owns the callbacks, and AR payments created through the contract-based REST API so reconciliation stays sane.
Architecture: Acumatica never talks to Daraja directly
The first design decision is the important one. Daraja delivers payment results by calling your HTTPS callback URL — unauthenticated except for IP provenance, with its own retry behaviour and occasional duplicate deliveries. You do not want that traffic hitting your ERP. Acumatica's endpoints expect authenticated sessions, and exposing a custom anonymous webhook handler inside the ERP web app is a security review you will lose.
So the shape is: a small middleware service (Laravel fits perfectly here) sits on the public internet, holds the Daraja credentials, receives the callbacks, and talks to Acumatica over the REST API as an authenticated client. Acumatica initiates collections by calling the middleware; results flow back the same way.
The collection flow: STK Push from an invoice
The user experience we want: a clerk opens an AR invoice, clicks Request M-Pesa Payment, the customer's phone lights up with the payment prompt, and thirty seconds later the invoice shows paid. The pieces:
- A
PXGraphExtensiononARInvoiceEntryadds the action. It validates the customer has a phone number in2547XXXXXXXXformat, then calls the middleware with the invoice number and balance. - The middleware requests an OAuth token from Daraja (
/oauth/v1/generate— tokens last an hour, cache them) and fires the STK Push:
POST /mpesa/stkpush/v1/processrequest HTTP/1.1
Host: api.safaricom.co.ke
Authorization: Bearer <token>
Content-Type: application/json
{
"BusinessShortCode": "174379",
"Password": "<base64(shortcode+passkey+timestamp)>",
"Timestamp": "20260707143000",
"TransactionType": "CustomerPayBillOnline",
"Amount": 25000,
"PartyA": "254712345678",
"PartyB": "174379",
"PhoneNumber": "254712345678",
"CallBackURL": "https://bridge.example.co.ke/daraja/stk-callback",
"AccountReference": "INV004521",
"TransactionDesc": "Invoice INV004521"
}
AccountReference is the load-bearing field: I always put the Acumatica invoice number there, because it's what lets the callback handler apply the payment to the right document without guesswork.
Callbacks: where all the real engineering lives
The STK callback arrives with ResultCode: 0 on success, carrying the M-Pesa receipt number, amount, and phone. Everything that can go wrong, does, so the handler needs four behaviours baked in:
- Idempotency. Daraja can deliver a callback more than once. The M-Pesa receipt number (
MpesaReceiptNumber, e.g.SGH4X8KL2M) is globally unique — store it with a unique constraint and drop duplicates on conflict. - Timeout reconciliation. Sometimes the callback simply never comes. Any request still pending after 2–3 minutes gets a Transaction Status query (
/mpesa/transactionstatus/v1/query) to learn the truth. Without this you'll have paid customers showing unpaid invoices. - Failure taxonomy.
ResultCode 1032is the user cancelling the prompt,1037is timeout/unreachable phone,1is insufficient funds. Surface these differently to the clerk — "customer cancelled" and "customer has no money" call for different phone calls. - Amount mismatches. Customers can pay via Paybill manually with the wrong amount. Take what the callback says, not what you asked for.
Recording it in Acumatica
On confirmed payment, the middleware creates an AR payment through the REST API, applied to the invoice, with the M-Pesa receipt in PaymentRef:
PUT /entity/Default/24.200.001/Payment HTTP/1.1
{
"Type": { "value": "Payment" },
"CustomerID": { "value": "ACME01" },
"PaymentMethod": { "value": "MPESA" },
"CashAccount": { "value": "MPESA-174379" },
"PaymentRef": { "value": "SGH4X8KL2M" },
"PaymentAmount": { "value": 25000.00 },
"DocumentsToApply": [
{ "DocType": { "value": "Invoice" },
"ReferenceNbr": { "value": "INV004521" } }
]
}
Set up a dedicated MPESA payment method and a cash account per shortcode. That cash account then reconciles against the M-Pesa statement (the daily settlement report from the Daraja portal or your bank's paybill statement) exactly like a bank account — receipt numbers in PaymentRef make the matching nearly automatic.
Customers will also pay your Paybill directly without an STK prompt. Register C2B confirmation URLs (/mpesa/c2b/v1/registerurl) and have the handler try to match BillRefNumber to an open invoice; unmatched payments become unapplied AR payments on a suspense customer for a human to resolve. Expect creative typing in that field — "INV 4521", "inv004521 thanks" — so match leniently.
Sandbox to production notes
Daraja's sandbox is behaviourally different from production — sandbox callbacks are more reliable than real ones, which trains false confidence. Going live requires the Safaricom go-live process for your shortcode and passkey, TLS on your callback URLs, and IP allowlisting if you can get it. Keep the middleware's logs verbatim (full callback JSON) for at least a year; when a customer disputes a payment in November, the September callback payload is your evidence.
Wrapping up
The integration is conceptually small — one action in Acumatica, two Daraja calls, one REST payment — but the reliability engineering around callbacks is the actual product. Idempotency on receipt numbers, status queries for the silent failures, lenient C2B matching, and a suspense path for the unmatchable. Get those four right and M-Pesa receipts flow into AR with less manual work than cheques ever did.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.