route.cloud

API documentation

JSON over HTTPS, OAuth2 client credentials, idempotent writes. Base URL https://route.cloud.or.ke/api/v1 · OpenAPI spec · Integration guide with code samples

1. Authentication

Exchange your client credentials for a bearer token. Tokens live one hour.

curl -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"

{ "access_token": "…", "token_type": "Bearer", "expires_in": 3600,
  "scope": "payments:write payments:read" }

Scopes

payments:read

Read transactions and their status

payments:write

Initiate STK Push collections

disbursements:write

Initiate B2C disbursements to customers

disbursements:approve

Approve held disbursements

transfers:write

Initiate B2B transfers to approved paybills and tills

reversals:write

Request transaction reversals

destinations:read

List destination platforms

destinations:write

Manage destination configuration

routing:read

Read routing rules and run the simulator

routing:write

Manage routing rules

webhooks:read

Read webhook delivery history

webhooks:write

Replay and manage webhook events

expenses:read

Read petty cash requests and their outcomes

expenses:write

Raise petty cash requests and record spending

reports:read

Read ledger, settlement and reconciliation data

2. Endpoints

MethodPathScopeWhat it does
POST /api/v1/oauth/token — Exchange credentials for a bearer token
POST /api/v1/oauth/revoke — Revoke a token immediately
GET /api/v1/oauth/introspect any What the current token can do
POST /api/v1/payment-requests payments:write Ask for a payment and mint a code
GET /api/v1/payment-requests payments:read List payment requests
GET /api/v1/payment-requests/{code} payments:read One request and every payment against it
POST /api/v1/payment-requests/{code}/stk-push payments:write Push a prompt for an existing code
POST /api/v1/payment-requests/{code}/cancel payments:write Void an unsettled request
POST /api/v1/payments/stk-push payments:write Send a PIN prompt to a customer handset
POST /api/v1/payments/simulate payments:write Simulate a paybill payment (sandbox only)
POST /api/v1/payments/qr payments:write Generate a scannable M-PESA QR
POST /api/v1/payments/standing-order payments:write Set up a recurring charge (Ratiba)
POST /api/v1/payments/disburse disbursements:write Send money to a customer (B2C)
POST /api/v1/payments/transfer transfers:write Transfer to an approved paybill or till (B2B)
POST /api/v1/payments/{id}/reverse reversals:write Reverse a collection
GET /api/v1/transactions payments:read Search the transaction register
GET /api/v1/transactions/{id} payments:read One transaction by id, reference or receipt
GET /api/v1/transactions/{id}/ledger reports:read Double-entry postings for a transaction
POST /api/v1/transactions/{id}/status-query payments:read Ask M-PESA what really happened
GET /api/v1/destinations destinations:read List destination platforms
GET /api/v1/destinations/{code}/balance reports:read Current balance owed to a platform
POST /api/v1/routing/simulate routing:read Dry-run an account reference
GET /api/v1/webhooks/events webhooks:read Delivery queue and status
POST /api/v1/webhooks/events/{uuid}/replay webhooks:write Re-queue a failed delivery
POST /api/v1/expenses expenses:write Ask for petty cash to be authorised
GET /api/v1/expenses expenses:read List your expense requests
GET /api/v1/expenses/{id} expenses:read One request and its decision
POST /api/v1/expenses/{id}/spend expenses:write Record spending against an approval
GET /api/v1/reports/summary reports:read Daily volume and value series
GET /api/v1/reports/float reports:read Paybill M-PESA float behind every payout
GET /api/v1/status — Health check

3. Idempotency

Send an Idempotency-Key header on every write. Replaying the same key with the same body returns the original response instead of moving money twice. Reusing a key with a different body is an error, not a silent overwrite.

curl -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"}'

4. Receiving webhooks

Final outcomes are pushed to your endpoint, signed, and retried with exponential backoff until you answer 2xx. Deduplicate on the event id — a retry may arrive after you have already processed it.

<?php
// Verify a RouteCloud webhook (PHP)
$secret  = getenv('ROUTECLOUD_WEBHOOK_SECRET');
$raw     = file_get_contents('php://input');
$header  = $_SERVER['HTTP_X_ROUTECLOUD_SIGNATURE'] ?? '';

// The header is t=<ts>,v1=<sig> and during a secret rotation it carries
// MORE THAN ONE v1 — the new secret's signature and the old one's. Do
// not use parse_str here: it keeps only the last v1, so whichever
// secret you hold, half the time it is not the one you are comparing
// against, and every webhook is rejected mid-rotation.
$timestamp = null;
$signatures = [];

foreach (explode(',', $header) as $part) {
    if (!str_contains($part, '=')) {
        continue;
    }
    [$k, $v] = explode('=', trim($part), 2);
    if ($k === 't')  { $timestamp = $v; }
    if ($k === 'v1') { $signatures[] = $v; }
}

if ($timestamp === null || abs(time() - (int) $timestamp) > 300) {
    http_response_code(400); exit('stale timestamp');
}

$expected = hash_hmac('sha256', $timestamp . '.' . $raw, $secret);

$valid = false;
foreach ($signatures as $candidate) {
    $valid = hash_equals($expected, $candidate) || $valid;
}

if (!$valid) {
    http_response_code(401); exit('bad signature');
}

$event = json_decode($raw, true);

// Deduplicate on $event['id'] — we retry until you return 2xx.
// Respond 2xx as soon as you have persisted it; do the slow work after.
http_response_code(200);
echo 'ok';

5. Errors

Every error is an RFC 7807 problem document, so you parse one shape.

{
  "type":   "https://route.cloud.or.ke/errors/validation_failed",
  "title":  "The request could not be processed. See `errors` for details.",
  "status": 422,
  "code":   "validation_failed",
  "errors": { "msisdn": "Customer mobile number must be a valid Kenyan mobile number." }
}

401 unauthenticated

Missing, expired or revoked bearer token.

403 insufficient_scope

Valid token, but the scope was not granted.

409 idempotency_in_flight

The same key is still being processed. Retry after a few seconds.

422 validation_failed

Field-level problems, listed in errors.

429 rate_limited

Back off for Retry-After seconds.

502 mpesa_error

Safaricom rejected the request; hint says what to do.

6. Network notes

Callback allowlist

Only Safaricom's published ranges may post to our callback endpoints: 196.201.214.0/24 196.201.213.0/24 196.201.212.0/24

Correlation IDs

Send X-Correlation-Id and we thread it through every log line, M-PESA call and webhook, so one id traces the whole payment.

Optional request signing

High-value clients can be required to sign requests: HMAC-SHA256(timestamp.METHOD.path.sha256(body)) in X-RC-Signature, with a 5-minute replay window.