POS payment integrations (Kenya)
See also: PAYMENT_METHODS_ARCHITECTURE.md for acceptor model, branch scoping, and tenant walkthroughs.
Payment methods at checkout
| Tile | Backend codes | Flow |
|---|---|---|
| Cash | cash |
Immediate sale |
| M-Pesa | mpesa, mpesa_paybill_manual, mpesa_buy_goods_manual, mpesa_paybill_stk, mpesa_buy_goods_stk, mpesa_c2b, kcb_stk, equity_stk |
Manual, STK, or C2B wait |
| Card | visa |
Terminal approval reference |
| Bank | bank_transfer |
Transfer reference |
| Other | other |
Free-text reference |
M-Pesa family methods show sub-chips — one per enabled payment acceptor in settings. Checkout is branch-aware: only acceptors where branch_id is empty or matches the current store appear.
Each sale can record mpesa_channel_id plus snapshot columns (mpesa_channel_paybill, mpesa_channel_till, mpesa_channel_account) for receipts and reconciliation.
Owner configuration
Settings → Integrations → POS payment settings (/pos/payments/settings)
The page has five independent sections, each with its own Save bar. Saving one section does not overwrite others.
| Section | Saves |
|---|---|
| Manual M-Pesa | Global defaults: txn code mandatory, verify-on-phone, legacy paybill/till fallbacks |
| POS channels | Payment acceptors — bank paybill, manual till/paybill, STK/C2B chips; branch scope; active/inactive |
| Daraja | Sandbox/production env, training mode, credentials, test STK |
| KCB Buni | Same pattern |
| Equity Jenga | Same pattern |
Typical tenant journeys
Kiosk — Equity 247247 only (no STK):
- Open POS channels → quick-add Equity Paybill 247247 (or add Bank paybill row).
- Enter account ref (6 digits), set Active, save channels only.
- Disable unused STK rows; skip Daraja/KCB/Equity sections entirely.
- At POS, cashier picks the Equity chip → hint shows paybill + account → enter SMS code after customer pays.
Multi-branch — different tills per outlet:
- Add one
manual_buy_goodsrow per till; set Branch on each row. - Save channels only. Checkout at Branch A shows Branch A tills; Branch B shows Branch B tills.
Petrol — many pumps, one branch:
- Add many
manual_buy_goodsrows (same branch or org-wide) with distincttill_numberand labels (e.g. "Pump 3"). - Each enabled row appears as its own sub-chip at checkout.
Section API
PATCH /pos/payments/settings/sections/{section}
section ∈ manual | channels | daraja | kcb | equity
Daraja (tenant provision)
Register callback URLs (per organization slug):
- STK:
https://{host}/webhooks/{slug}/mpesa/stk-callback - C2B:
https://{host}/webhooks/{slug}/mpesa/c2b/confirmation
Async checkout flow
- Cashier selects M-Pesa method and acceptor sub-chip (e.g. STK Paybill, Equity 247247, Pump 2 till).
- Checkout sends
mpesa_channel_idwith the cart (hidden field + JS sync). - Complete sale (STK/C2B) calls
POST /pos/checkout/payments/initiatewith cart +mpesa_channel_id. - STK pushes to the customer phone (or C2B shows account reference from the selected acceptor).
- Frontend polls
GET /pos/checkout/payments/intents/{id}untilpaid. - Daraja/KCB webhook marks
PaymentIntentpaid; optional auto-fulfillment creates the sale. - When the webhook marks the intent paid, the backend auto-posts the sale (stock, events, parked-cart clear). The POS polls until
checkout_completeis true, then redirects to receipt — no second Complete sale tap. - If auto-post did not run (e.g. stock error), the cashier can still submit with
payment_intent_idas a fallback.
Simulation vs sandbox STK testing
Two different modes — do not confuse them:
| Mode | Setting | POS behaviour |
|---|---|---|
| Sandbox STK (real test push) | Active environment = Sandbox, Training mode off, sandbox credentials saved | Calls Daraja/Buni/Jenga sandbox API — customer phone gets STK prompt |
| Production STK (live) | Active environment = Production, production credentials saved, training mode off | Calls production API — real money |
| Offline training | Training mode on | No API call; checkout auto-completes via simulate-paid (cashier drills only) |
Sandbox test flow (Daraja example)
- Integrations → POS payment setup → Daraja
- Set Active environment at POS = Sandbox
- Leave Training mode off
- Enter Daraja sandbox credentials (consumer key, secret, shortcode e.g. 174379, passkey from developer.safaricom.co.ke)
- Register STK callback URL in Daraja portal (copy from settings page)
- Save settings
- Optional — Test STK from settings: enter a mobile number in Send test STK push (KES 1) and tap Send test push — no sale is created
- Or at POS: add items → choose STK Paybill/Till → enter sandbox test number (e.g. 254708374149) → Complete sale
- Phone receives Daraja sandbox STK prompt
Same pattern for KCB Buni (sandbox client ID/secret from Buni portal) and Equity Jenga (sandbox API key/secret).
Test STK from payment settings
Each rail (Daraja, KCB, Equity) has a Send test STK push panel on /pos/payments/settings:
- Sends KES 1 via the active environment’s saved credentials (
POST /pos/payments/settings/test-stk) - Requires Training mode off and credentials saved for the selected environment
- Does not create a
PaymentIntentor sale — use POS checkout for full async flow testing - Production environment shows a confirmation dialog before sending
- Training mode and Active environment toggles update the panel immediately (no save required for enable/disable state)
Callbacks still need a reachable webhook URL (staging/ngrok) if you want to verify the provider callback path end-to-end.
Go-live
- Enter production credentials in the production credential block (keep sandbox keys for reference)
- Set Active environment at POS = Production
- Ensure Training mode is off
- Register production callback URLs with the provider
Legacy single credential blobs are migrated automatically into the sandbox bucket on read.
Simulation (offline training only)
Set Training mode on a rail, or POS_MPESA_SIMULATE=true globally. Initiate creates an intent without calling the provider; dev/tests use POST .../intents/{id}/simulate-paid.
Plugins
- Daraja —
App\Services\Pos\Payments\Gateways\DarajaStkGateway - KCB Buni —
BuniKcbStkGateway(credentials when available) - Equity Jenga —
JengaEquityStkGateway(credentials when available)