
# 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](https://kibanda.africa/docs/customers). |
| **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 with `payment.status: partial`; the rider collects only the balance (cash or an M-Pesa prompt at the door). Events: `order.deposit_paid`, then `order.paid` when 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`](https://kibanda.africa/docs) 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.
