route.cloud
Gateway operational · PRODUCTION

One paybill.
Every platform paid correctly.

RouteCloud sits between Safaricom's Daraja APIs and your internal systems. Customers pay a single M‑PESA paybill; the gateway reads the account reference, decides which platform the money belongs to, posts it to the ledger and notifies that platform — in one hop, with a full audit trail behind it.

4320161Collection paybill
5Connected platforms
98Payments routed (30d)
87.8%Success rate (30d)
84.9%Callback acceptance

How a payment travels

Five steps, every one of them logged and replayable.

1

Customer pays

A payment lands on the shared paybill from the M‑PESA menu, an STK push, or a QR scan. Safaricom calls our validation endpoint first.

2

Reference is read

The account number is normalised — spaces, dashes and the usual keypad typos (O for 0, I for 1) are corrected before matching.

3

Rules decide

Ordered rules match on prefix, range, regex or amount. One payment can be split across several platforms by percentage — the parts always re-sum exactly.

4

Ledger is posted

Double-entry postings credit the destination and record the fee. The webhook event is written in the same database transaction, so it can never be lost.

5

Platform is notified

A signed webhook is delivered with exponential backoff and a dead-letter queue. Every attempt — connected or failed — is kept with its response body.

6

Nothing is lost

A payment that matches no rule is never rejected silently: it goes to a suspense queue with a suggested destination for finance to confirm.

Initiate a collection

OAuth2 client credentials, JSON over HTTPS, idempotent by design.

# 1 — get a token (valid one hour)
curl -s -X POST https://route.cloud.or.ke/api/v1/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials -d scope="payments:write payments:read"

# 2 — push an STK prompt to the customer's handset
curl -s -X POST https://route.cloud.or.ke/api/v1/payments/stk-push \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: order-88213" \
  -H "Content-Type: application/json" \
  -d '{
        "msisdn":      "254712345678",
        "amount":      1500,
        "account_ref": "SCH-4821",
        "description": "Term 2 fees"
      }'

# 202 Accepted
{
  "transaction_id": "9f2c1e40-5b7a-4d1e-9a33-0c7f9b21d5aa",
  "reference":      "RC-8FK2QD9L",
  "status":         "awaiting_callback",
  "destination":    "SCHOOLPAY",
  "customer_message": "Success. Request accepted for processing"
}

What you get on the other side

A signed webhook, once per destination, retried until you acknowledge it.

POST https://your-platform.example/hooks/routecloud
X-RouteCloud-Signature: t=1755168000,v1=9c4f…
X-RouteCloud-Event: payment.received
X-RouteCloud-Delivery: 3f9a2b1c-…

{
  "id":   "3f9a2b1c-7d44-4e91-b0e2-71c8a5f3d902",
  "type": "payment.received",
  "data": {
    "reference":         "RC-8FK2QD9L",
    "amount":            1500.00,
    "fee":               15.00,
    "net_amount":        1485.00,
    "account_reference": "4821",
    "customer":          { "msisdn": "254712***678" },
    "mpesa":             { "receipt": "SGJ4K2LM9P" },
    "reconciliation_id": "REC-20260814-A7F2C91D0B"
  }
}

Built for the awkward parts

The failure modes that actually cost money in production.

Duplicate callbacks

Safaricom retries. A unique constraint on the M‑PESA receipt means a replayed confirmation can never create a second credit.

Callbacks that never arrive

A sweeper picks up anything still awaiting a callback and asks the Transaction Status API what really happened, rather than guessing.

Mistyped account numbers

Confusable characters are retried automatically, and finance can pin a permanent alias so the same payer never lands in suspense twice.

Double disbursement

Every payout carries a unique OriginatorConversationID, and payouts above a threshold require a second approver who cannot be the requester.

Tampered records

The audit trail is hash-chained. Editing or deleting any historical entry breaks the chain, and the verifier reports exactly where.

Stolen credentials

Staff sign-in is password plus SMS OTP, sessions are bound to IP and device, and every new account needs an administrator to authorise each permission.