Payments
Customers choose at checkout:
| Method | How it works |
|---|---|
| M-Pesa prompt | An STK push to their phone; they enter their PIN. |
| Card / other | A secure checkout page (card, M-Pesa, bank) in WhatsApp's browser. |
| Pay on delivery | Cash or M-Pesa when the rider arrives — on the shop's own terms (below). |
At the door, the rider can send an M-Pesa prompt to the customer's phone from their WhatsApp, and hands over once it's paid — no cash to carry. Or they record cash, which shows as owed by that rider until the shop marks it handed in.
A payment is only ever marked paid after we ask the provider directly; webhooks and redirects are just nudges. Money that arrives for a cancelled order, or twice, raises order.refund_needed.
Payment terms (the shop decides)
Each shop sets its own terms in the dashboard (Settings → Payment terms). Kibanda applies exactly those and nothing else:
| Setting | Effect |
|---|---|
| Pay on delivery: on / off | Off = every delivery is paid in full (M-Pesa or card) before it leaves. Pickup orders can still pay at the counter. |
| Rules (each optional) | Orders above KES X · customers with fewer than N delivered orders · customers with N or more refusals. |
| When a rule matches | Full payment first, or a deposit first — the delivery fee or a percentage — with the rest on delivery. |
| Per customer | Your shop's rules (default) · Always pays before delivery · Trusted: may always pay on delivery. Set on the Customers page or with payment_terms on the customer. |
| Per order | The shop can ask for payment first on any confirmed order that hasn't left — full or the deposit — from WhatsApp (the order card), the dashboard, POST /v1/orders/{id}/require_payment, or the MCP require_payment tool. |
How it plays out:
- Deposit: the order waits (
status: awaiting_payment,payment.upfront_due= the deposit) and the shop isn't alerted until it's paid. Then it's confirmed withpayment.status: partial; the rider collects only the balance (cash or an M-Pesa prompt at the door). Events:order.deposit_paid, thenorder.paidwhen the whole total is in. - Asked to pay first: the customer gets the amount with M-Pesa and card buttons; the order can't be sent with a rider until it's paid, and the customer can't switch it back to pay on delivery. Event:
order.payment_required(payment.upfront_required_by: "shop").
The shop's current terms are on GET /v1/shop as payment_terms.
Bring your own
By default payments go through Kibanda's provider. A shop can connect its own account (dashboard → Payments) so money goes straight to them:
| Account | M-Pesa prompts | Card page | You need |
|---|---|---|---|
| M-Pesa till (Buy Goods) | ✓ | A Daraja app with Lipa Na M-Pesa Online: consumer key/secret, passkey, the store number and the till | |
| M-Pesa paybill | ✓ | The same, for the paybill | |
| Paystack | ✓ | ✓ | Secret key (sk_live_…); set the webhook URL we show you |
| Flutterwave | ✓ | ✓ | Secret key (FLWSECK-…) |
Credentials are checked with the provider before they're used, encrypted at rest (AES-256-GCM, bound to the account), and never shown again. Each payment records which account took it. If a shop's account stops working, new payments fall back to Kibanda's, so customers can always pay.
M-Pesa direct (Daraja): Safaricom calls back to a unique, unguessable URL per payment, and we still confirm with Safaricom's STK query before marking it paid, so a forged callback can't mark an order paid. The receipt number (e.g. TJK3H2K9LP) is saved on the payment.