paylod
Docs

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.dev

Authentication

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_KEY

Idempotency

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.

POST/functions/v1/collect

Initiate 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

Authorization
stringrequired

Bearer API key (mp_live_…).

Idempotency-Key
stringoptional

Replay guard. A repeat with the same key + body returns the first response.

Body

amount
integerrequired

Whole KES, 1–150000. M-Pesa rejects decimals.

phone
stringrequired

Kenyan MSISDN, any common form (0712…, 254712…, +254712…). Normalized for you.

accountReference
stringoptional

Shown on the handset + statement. 1–12 chars. Defaults to "collect".

description
stringoptional

Free-text label, 1–64 chars. Defaults to "Payment".

metadata
objectoptional

Opaque 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.

GET/functions/v1/status/:id

Read 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

id
stringrequired

The 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-event
stringoptional

Event type: payment.success or payment.failed.

x-webhook-id
stringoptional

Unique delivery id — use it to dedupe retries.

x-webhook-signature
stringoptional

HMAC 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 reference

Beyond 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:

POST/qr-generate
Generate a dynamic M-Pesa QR for the amount.
POST/c2b-register
Register C2B validation/confirmation URLs for a paybill.
POST/transaction-status
Query the status of any M-Pesa transaction by receipt/ID.
POST/account-balance
Fetch the till/paybill account balance.