paylodDocs
API Reference
Everything you need to accept M-Pesa payments. Two endpoints to move money and read its result, plus signed webhooks — no callback hosting on your side.
Base URL
All requests go to your hosted origin. Paths live under /functions/v1.
https://paylod.devAuthentication
Authenticate every request with an API key as a bearer token. Create keys on the API Keys tab of an application. Keys are environment-scoped: mp_test_… for Sandbox, mp_live_… for Production. The key determines which application (till/paybill) and environment the request runs against — so you never send a shortcode or credentials over the wire.
Authorization: Bearer mp_live_YOUR_API_KEYIdempotency
Pass an Idempotency-Key header (or idempotencyKey in the body) on /collect. A retry with the same key and identical body replays the original response instead of triggering a second STK push — safe against network retries and double-clicks.
/functions/v1/collectInitiate an STK Push. The customer gets an M-Pesa prompt on their handset; when they enter their PIN, funds settle directly to your till. Returns immediately with a paymentId — the final result arrives via the hosted callback (read it back with GET /status or a webhook).
Headers
AuthorizationBearer API key (mp_live_…).
Idempotency-KeyReplay guard. A repeat with the same key + body returns the first response.
Body
amountWhole KES, 1–150000. M-Pesa rejects decimals.
phoneKenyan MSISDN, any common form (0712…, 254712…, +254712…). Normalized for you.
accountReferenceShown on the handset + statement. 1–12 chars. Defaults to "collect".
descriptionFree-text label, 1–64 chars. Defaults to "Payment".
metadataOpaque key/values echoed back to you in the webhook. We never inspect it.
Request
curl -X POST https://paylod.dev/functions/v1/collect \
-H "Authorization: Bearer mp_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order_2041" \
-d '{
"amount": 10,
"phone": "254712345678",
"accountReference": "INV-2041",
"description": "Order #2041",
"metadata": { "orderId": "2041" }
}'Response · 202
HTTP/1.1 202 Accepted
{
"paymentId": "e69e5c00-8a01-44ed-b003-48e2e86a7c9e",
"status": "pending",
"checkoutRequestId": "ws_CO_010720261905029287161380"
}Errors
401Missing or invalid API key.400Malformed JSON, or Daraja credentials not configured for this app/env.422Body failed validation (e.g. amount ≤ 0, bad phone).502Daraja rejected the STK push (upstream error passed through).Every M-Pesa result code is decoded in the error reference.
/functions/v1/status/:idRead a payment's current state — the zero-infrastructure way to get the result (no webhook receiver needed). If the payment is still pending, we run a live M-Pesa STK Query and settle it on the spot before responding. Scoped to the key's application — another app's payment returns 404.
Path
idThe paymentId returned by /collect.
Request
curl https://paylod.dev/functions/v1/status/e69e5c00-8a01-44ed-b003-48e2e86a7c9e \
-H "Authorization: Bearer mp_live_YOUR_API_KEY"Response · 200
HTTP/1.1 200 OK
{
"id": "e69e5c00-8a01-44ed-b003-48e2e86a7c9e",
"status": "success",
"mpesaReceipt": "UG1F3A1U7J",
"resultCode": 0,
"resultDesc": "The service request is processed successfully."
}status is one of pending, success, failed. On failure, resultCode / resultDesc carry the decoded M-Pesa reason (e.g. 1032 cancelled, 1037 timeout / no response) — all listed in the error reference.
Webhooks
Optional push delivery. Register an endpoint URL on an application's Endpoints & Webhookstab and we POST a signed event the moment a payment settles. If you'd rather not run a receiver, skip this and poll GET /status instead — every payment is recorded either way.
Delivery headers
x-webhook-eventEvent type: payment.success or payment.failed.
x-webhook-idUnique delivery id — use it to dedupe retries.
x-webhook-signatureHMAC signature, format t=<unix>,v1=<hex>.
Example delivery
POST https://your-app.com/webhooks/mpesa
x-webhook-event: payment.success
x-webhook-id: 8f3c…
x-webhook-signature: t=1751394302,v1=5a2f…c9
{
"type": "payment.success",
"created": 1751394302,
"data": {
"paymentId": "e69e5c00-…",
"applicationId": "…",
"env": "production",
"status": "success",
"amount": 10,
"phone": "254712345678",
"accountRef": "INV-2041",
"mpesaReceipt": "UG1F3A1U7J",
"checkoutRequestId": "ws_CO_…",
"resultCode": 0,
"resultDesc": "The service request is processed successfully."
}
}Verify the signature (Node)
import crypto from "node:crypto";
// Express raw-body handler
export function verify(req, secret) {
const header = req.headers["x-webhook-signature"]; // "t=...,v1=..."
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const signed = `${parts.t}.${req.rawBody}`; // timestamp + raw JSON
const expected = crypto
.createHmac("sha256", secret)
.update(signed)
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
// reject if !ok, or if Date.now()/1000 - parts.t > 300 (replay window)
return ok;
}The signature is an HMAC-SHA256 over `${t}.${rawBody}`keyed by your endpoint's signing secret (shown once when you add the endpoint). Compute it over the raw request body, constant-time compare against v1, and reject deliveries whose t is older than ~5 minutes to blunt replay. Respond 2xx to acknowledge; non-2xx is retried with backoff.
Error codes
Every Safaricom M-Pesa result and error code is decoded — searchable by number or description — on a dedicated page.
Browse the error referenceBeyond STK Push
The same hosted, credential-abstracted model backs the rest of the Daraja surface. These endpoints live under /functions/v1 and authenticate with the same bearer API key:
/qr-generate/c2b-register/transaction-status/account-balance