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.
# 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.
# 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.
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.
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.
# Every request is authenticated with your secret key
curl https://api.malipo.dev/api/v1/payments \
-H "Authorization: Bearer mlp_live_4f9c0b3a8e21…"
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.
# 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
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
Customer phone in any Kenyan format — 07…, +254…, 254…, or 9-digit.
amount
Amount in KES. Integer or decimal.
reference
Your reference, shown on the customer's prompt and echoed back.
description
Short description shown on the customer's prompt.
idempotency_key
Replay-safe key. Also accepted as the Idempotency-Key header.
Request
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
{
"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
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
Amount in KES. Integer or decimal.
reference
Your reference for this payment. Used for tracking.
idempotency_key
Replay-safe key. Also accepted as the Idempotency-Key header.
Request
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
{
"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"
}
}
GET /payments?reference=INVOICE-12
to detect when it settles.
Retrieve a payment
Fetch the current state of a single transaction by its id. Check this to track a transaction to its terminal state.
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
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.
curl "https://api.malipo.dev/api/v1/payments?limit=2&reference=ORDER-8842" \
-H "Authorization: Bearer mlp_live_4f9c0b3a8e21…"
{
"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.
# 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.
# 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.
Payment states
A transaction's state moves through this lifecycle.
Created; the prompt is being sent.
Prompt delivered; awaiting the customer.
Customer paid. Wallet credited. Terminal.
Cancelled, wrong PIN, or insufficient funds. Terminal.
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.
{
"error": {
"code": "validation",
"message": "Request failed validation.",
"details": {
"amount": ["must be greater than 0"]
}
}
}