Quick Start

Your first payment in 3 commands

No SDK to install. No complex setup. Just curl and JSON.

Step 1 — Get your API key

Sign up, create an organization, and copy your API key from the dashboard. Keys start with mlp_test_ in test mode and mlp_live_ in production.

Step 2 — Send an STK Push

This sends a payment prompt to your customer's phone. They enter their M-PESA PIN and the funds are credited to your wallet.

bash
# Send an STK Push to a customer's phone
curl -X POST https://api.malipo.dev/api/v1/payments/stk_push \
  -H "Authorization: Bearer mlp_test_•••" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_8842" \
  -d '{
    "phone": "+254712345678",
    "amount": 2500,
    "reference": "ORDER-8842"
  }'

Save the id from the response — you'll need it to check the result.

Step 3 — Check the result

STK Push is asynchronous. Check the status every few seconds until the state becomes success or failed.

bash
# Check for the result (replace 123 with the id from the response)
curl https://api.malipo.dev/api/v1/payments/123 \
  -H "Authorization: Bearer mlp_test_•••"

# → Check the "state" field. "success" = paid. "failed" = declined.
That's it — you're live. Read on for the full API reference, Paybill flows, and error handling.

Introduction

Malipo is a REST API for collecting from and disbursing to M-PESA numbers in Kenya. All requests are JSON over HTTPS. Responses are JSON with a consistent envelope shape.

Base URL https://api.malipo.dev/api/v1

STEP 1

POST a payment

STEP 2

Customer approves

STEP 3

Wallet credit

Authentication

Authenticate every request with your secret API key in an Authorization header as a Bearer token. Create and rotate keys from your dashboard under Apps. Keep them server-side — never ship a key in client code.

bash
# Every request is authenticated with your secret key
curl https://api.malipo.dev/api/v1/payments \
  -H "Authorization: Bearer mlp_live_4f9c0b3a8e21…"
Requests without a valid key return 401 with an error code of missing_api_key, invalid_api_key, or api_key_revoked.

Rate limits

Each API key is limited to 120 requests per minute. When you exceed it the API responds with 429 Too Many Requests, a Retry-After header, and a retry_after_seconds field in the body. Back off for that long and retry.

429 Too Many Requests
# HTTP/1.1 429 Too Many Requests
# Retry-After: 12
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry in 12s.",
    "retry_after_seconds": 12
  }
}

Idempotency

Safely retry payment creation without double-charging by sending an Idempotency-Key header (or an idempotency_key body field). If Malipo has already seen that key, it returns the original transaction unchanged instead of starting a new one. Use a unique value per logical operation — an order id works well.

STK Push

POST /api/v1/payments/stk_push

Pushes a payment prompt to the customer's phone. Returns 201 immediately with a transaction in the pending state. Check the retrieve endpoint for the final outcome.

Body parameters

phone
string required

Customer phone in any Kenyan format — 07…, +254…, 254…, or 9-digit.

amount
number required

Amount in KES. Integer or decimal.

reference
string optional

Your reference, shown on the customer's prompt and echoed back.

description
string optional

Short description shown on the customer's prompt.

idempotency_key
string optional

Replay-safe key. Also accepted as the Idempotency-Key header.

Request

bash
curl -X POST https://api.malipo.dev/api/v1/payments/stk_push \
  -H "Authorization: Bearer mlp_live_4f9c0b3a8e21…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_8842" \
  -d '{
    "phone": "+254712345678",
    "amount": 2500,
    "reference": "ORDER-8842",
    "description": "Premium plan — monthly"
  }'

Response

201 Created · application/json
{
  "data": {
    "id": 1429,
    "type": "stk_push",
    "state": "pending",
    "amount": "2500.00",
    "phone": "254712345678",
    "reference": "ORDER-8842",
    "description": "Premium plan — monthly",
    "mpesa_receipt": null,
    "result_code": null,
    "result_desc": null,
    "checkout_request_id": "ws_CO_02062026…",
    "merchant_request_id": "29115-34620561-1",
    "idempotency_key": "order_8842",
    "inserted_at": "2026-06-02T19:30:00Z",
    "updated_at": "2026-06-02T19:30:00Z"
  }
}

Paybill Intent

POST /api/v1/payments/paybill

Creates a manual Paybill payment intent — no STK Push. Returns a paybill business number and a pay_code account number. Tell your customer to pay the business number manually, using the pay_code as the account number. Check by reference to detect settlement.

Body parameters

amount
number required

Amount in KES. Integer or decimal.

reference
string optional

Your reference for this payment. Used for tracking.

idempotency_key
string optional

Replay-safe key. Also accepted as the Idempotency-Key header.

Request

bash
curl -X POST https://api.malipo.dev/api/v1/payments/paybill \
  -H "Authorization: Bearer mlp_live_4f9c0b3a8e21…" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1500,
    "reference": "INVOICE-12"
  }'

Response

201 Created · application/json
{
  "data": {
    "id": 1430,
    "type": "c2b",
    "state": "pending",
    "amount": "1500.00",
    "phone": null,
    "reference": "INVOICE-12",
    "paybill": "4005871",
    "pay_code": "C4UJX9S",
    "inserted_at": "2026-06-02T19:35:00Z",
    "updated_at": "2026-06-02T19:35:00Z"
  }
}
The customer pays manually via M-PESA → Lipa na M-PESA → Paybill. They enter the business number and the pay_code as the account number. Call GET /payments?reference=INVOICE-12 to detect when it settles.

Retrieve a payment

GET /api/v1/payments/:id

Fetch the current state of a single transaction by its id. Check this to track a transaction to its terminal state.

bash
curl https://api.malipo.dev/api/v1/payments/1429 \
  -H "Authorization: Bearer mlp_live_4f9c0b3a8e21…"

# → 200 OK with the same { "data": { … } } shape,
#   now "state": "success" and "mpesa_receipt": "UEI6C48J44".

List payments

GET /api/v1/payments

Returns your most recent transactions, newest first. Use limit (1–200, default 50) to size the page. Filter by reference to find Paybill settlements. The response includes a meta object with the count and limit.

bash
curl "https://api.malipo.dev/api/v1/payments?limit=2&reference=ORDER-8842" \
  -H "Authorization: Bearer mlp_live_4f9c0b3a8e21…"
200 OK · application/json
{
  "data": [
    { "id": 1429, "state": "success", "amount": "2500.00", "phone": "254712345678" },
    { "id": 1428, "state": "failed",  "amount": "500.00",  "phone": "254700111222" }
  ],
  "meta": { "count": 2, "limit": 2, "reference": "ORDER-8842" }
}

Tracking Payments

Malipo's payment flows are asynchronous. When you create a payment, the API returns immediately with a transaction in a non-terminal state. You check the status to find out if the customer paid.

Check by transaction ID (STK Push)

After creating an STK Push, save the id from the response. Call GET /payments/:id every few seconds until the state is no longer pending or sent. Most transactions settle within 30 seconds.

bash
# Check every 3-5 seconds until state is no longer pending/sent
curl https://api.malipo.dev/api/v1/payments/1429 \
  -H "Authorization: Bearer mlp_test_•••"

# Typical response after customer pays:
# "state": "success", "mpesa_receipt": "UEI6C48J44"

Check by reference (Paybill)

For Paybill intents, you don't know when the customer will pay. Call GET /payments?reference=YOUR_REF periodically (e.g., every 8 seconds) to detect settlement. The first matching success transaction is your payment.

bash
# Check by reference to detect a Paybill settlement
curl "https://api.malipo.dev/api/v1/payments?reference=INVOICE-12" \
  -H "Authorization: Bearer mlp_test_•••"

# If the customer has paid, you'll see a success transaction.
# If empty, keep checking every 8 seconds.

When to stop checking

Stop when the state is success, failed, or timeout. These are terminal states — they will never change. If you get timeout, the customer can still pay manually via Paybill (if you created a Paybill intent), so keep checking by reference.

A simple check loop: start a timer, call the API, check the state, repeat until terminal. Most STK Push transactions resolve in under 30 seconds. Paybill can take minutes or hours.

Payment states

A transaction's state moves through this lifecycle.

pending

Created; the prompt is being sent.

sent

Prompt delivered; awaiting the customer.

success

Customer paid. Wallet credited. Terminal.

failed

Cancelled, wrong PIN, or insufficient funds. Terminal.

timeout

No response within the SLA window. Terminal.

Errors

Malipo uses conventional HTTP status codes and a consistent JSON error envelope: an error object with a stable code and a human message (validation errors add details).

400 / 422
validation · invalid_phone

The request body failed validation.

401
missing_api_key · invalid_api_key

Authentication failed.

404
not_found

No such transaction on your account.

429
rate_limited

Too many requests — back off and retry.

503
psp_unavailable

The payment network is momentarily unavailable.

422 Unprocessable Entity · application/json
{
  "error": {
    "code": "validation",
    "message": "Request failed validation.",
    "details": {
      "amount": ["must be greater than 0"]
    }
  }
}
Malipo

The mobile payments platform for every business. Charge Kenyan customers via STK Push, collect through C2B Paybill, and disburse via B2C — all through one API. An Uhandisi Hub project.

Product

© 2026 Uhandisi Hub. All rights reserved.

Built in Nairobi · serving businesses worldwide.