API reference
Payments API: STK Push and payment status
POST /collect to trigger an STK Push and GET /status/:id to read the outcome — the two endpoints that move and read money in.
POST /collect
Start an STK Push. The customer receives an M-Pesa prompt on the handset. When the customer enters the PIN, the funds settle directly to your till. The endpoint returns a paymentId immediately. The final result arrives on the hosted callback. You read that result with GET /status/:id or with a webhook.
POST https://paylod.dev/functions/v1/collectHeaders
| Name | Type | Required | Description |
|---|---|---|---|
Authorization | string | Required | Bearer API key (mp_live_… / mp_test_…). |
Content-Type | string | Required | application/json. |
Idempotency-Key | string | Send it | The double-charge guard. The idempotency key names one payment attempt. Make a new key for each attempt, not for each order and not for each product. If you repeat the same key and the same body, paylod returns the original payment. paylod does not send a second STK prompt, even for concurrent duplicates. If you repeat the same key with a different body, paylod returns 409. If an interrupted provider call spent the key, paylod returns 409 indeterminate. In that case, read the payment status, then retry with a new key. If you omit the idempotency key, every call is a new charge. A double-clicked Pay button then sends two prompts and makes two debits. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | Required | Whole KES, 1–150000. M-Pesa rejects decimals. |
phone | string | Required | Kenyan MSISDN in any common form (0712…, 254712…, +254712…). paylod normalises it for you. |
accountReference | string | Optional | Your correlation id, such as an invoice number or an order number. 1–12 chars. The webhook returns this value as accountRef. The GET /status/:id read carries only id, status, mpesaReceipt, resultCode and resultDesc. Safaricom shows this value to the customer only on a Paybill (CustomerPayBillOnline), where it is the account number. A Till / Buy Goods shortcode does not show it. The default is a short prefix of the paymentId, so an omitted reference stays unique and traceable. This value is a label, not a lock. It does not deduplicate anything. Only Idempotency-Key prevents a second charge. |
description | string | Optional | Free-text label, 1–64 chars. Defaults to Payment. |
metadata | object | Optional | Opaque keys and values that paylod stores with the payment. paylod never inspects them. /status and the webhook do not return them. Key your records on paymentId. |
Example
const ack = await paylod.collect({
amount: 10,
phone: "254712345678",
idempotencyKey: attempt.id,
accountReference: "INV-2041",
description: "Order #2041",
metadata: { orderId: "2041" },
});
// ack.paymentId, ack.status ("pending"), ack.checkoutRequestId{
"paymentId": "e69e5c00-8a01-44ed-b003-48e2e86a7c9e",
"status": "pending",
"checkoutRequestId": "ws_CO_010720261905029287161380"
}Send `Idempotency-Key`, and make one key for each payment attempt. If you repeat the same key, you get the same paymentId and checkoutRequestId back. paylod does not send a second prompt to the handset, even for concurrent duplicates. If you omit the idempotency key, a double-clicked Pay button, a refreshed tab or a retried request each send a fresh STK prompt. Each one charges the customer again. Do not key on an order id or a product id. A key is spent once you use it, so a reused key replays an old payment. A retry after a wrong PIN is a new charge and needs a new key.
*A `409` indeterminate means stop. It does not mean retry. If an earlier request under this idempotency key stopped while the Daraja call was in flight, paylod does not repeat that call. The money may already have moved. A timeout is not evidence that the money did not move. For money, at-most-once is better than at-least-once. Read `GET /status/:id` first. If nothing happened, retry with a new* key. The same key returns this 409 again.
Errors
| Status | When |
|---|---|
401 | The API key is missing or invalid. |
400 | The JSON is malformed, or you did not set Daraja credentials for this application and environment. |
409 | You reused the idempotency key with a different body. Or the first request is still in flight (Retry-After). Or an interrupted provider call spent the key. The third case is indeterminate: read the status, then retry with a new key. |
422 | The body failed validation. For example, amount ≤ 0, or paylod cannot parse the phone number. |
429 | You reached the rate limit. paylod limits each API key and each phone number. |
502 | Daraja rejected the STK Push. paylod passes the upstream error through. |
GET /status/:id
Read the current state of a payment. This endpoint needs no webhook endpoint and no other infrastructure. If the payment is still pending, paylod runs a live M-Pesa STK Query. paylod settles the payment before it responds. The API key limits this read to its own application. A payment of another application returns 404.
GET https://paylod.dev/functions/v1/status/:idPath
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Required | The paymentId returned by POST /collect. |
Example
// check() reads the payment and decodes it into a renderable PaymentOutcome.
const outcome = await paylod.check("e69e5c00-8a01-44ed-b003-48e2e86a7c9e");
if (outcome.paid) fulfil(outcome.receipt);
else console.log(outcome.message);{
"id": "e69e5c00-8a01-44ed-b003-48e2e86a7c9e",
"status": "success",
"mpesaReceipt": "UG1F3A1U7J",
"resultCode": 0,
"resultDesc": "The service request is processed successfully."
}Status values
| Value | Meaning |
|---|---|
pending | paylod sent the prompt. The customer did not answer it yet. This state is not a failure. |
success | The payment settled. mpesaReceipt is the M-Pesa receipt number. |
failed | The payment did not happen. resultCode and resultDesc carry the M-Pesa reason. |
On a failure, resultCode is the raw Safaricom code: 1032 cancelled, 1037 timeout, 2001 wrong PIN. The error reference decodes all of these codes.