Appearance
Public routes
These routes need no key and no session. The hub payer pages call them.
Use the merchant API from your server
A hosted checkout has a fixed price. The routes below are what the payer browser calls. They are rate limited. See Limits.
Call the payments host for the country that holds the money. A Kenyan checkout posted to the Ugandan host fails. The checkout id carries the country (cs_ke_…). A handle response includes served_here.
Prefix: /v1/payments/public.
Resolve a handle or link
GET /handles/{handle}handle matches ^[A-Za-z0-9._-]{1,64}$. It can be a username or a link address.
json
{
"ok": true,
"description": "Handle resolved",
"data": {
"name": "asha-rent",
"username": "asha",
"title": null,
"amount": null,
"display_name": "Asha",
"profile_image_url": null,
"home_country": "KE",
"served_here": true,
"currency": "KES",
"rail": "mpesa",
"min": "100",
"max": "15000000",
"paybill": null,
"accepting": true
}
}| Field | |
|---|---|
name | The address in the URL. On a link this differs from username |
username | Account owner |
title, amount | Link title and suggested amount, minor units, as a string. null on a handle or an open link |
served_here | false when this host does not hold the money. Money fields are then null and accepting is false. Call the home_country host |
currency, rail, min, max | How this host collects. Amounts are minor units |
paybill | M-Pesa Pay Bill business number, or null. The account number the payer types is name |
accepting | The wallet exists and can take a deposit |
404 code 2710: no account with that name. 503 code 2752: the ledger did not answer. That is not the same as accepting: false.
Pay a handle or link
POST /checkoutSends a prompt to the payer phone. Idempotency-Key is required (1 to 128 characters).
| Field | Required | |
|---|---|---|
handle | Yes | Address to pay |
amount | Yes | Minor units |
phone | Yes | Payer number. 9 to 15 digits, country code, no + |
rail | No | Default is the country's mobile-money rail |
202 when a prompt is sent. 200 when the key is a replay. Both use description Checkout initiated. On a replay, data.message is Duplicate request — returning existing intent.
json
{
"ok": true,
"description": "Checkout initiated",
"data": {
"intent_id": "pi_8c1e4f0a7b3d92e5a6f01c48",
"status": "pending",
"amount": "150000",
"currency": "KES",
"rail": "mpesa",
"message": "STK Push sent to your phone. Enter your M-Pesa PIN to complete."
}
}Read an intent
GET /intents/{id}id is an intent_id (pi_…). The response has no phone, no receipt, and no timestamps.
json
{
"ok": true,
"description": "Intent retrieved",
"data": {
"intent_id": "pi_8c1e4f0a7b3d92e5a6f01c48",
"status": "completed",
"amount_settled": "150000",
"currency": "KES"
}
}status is created, reserved, submitted, pending, completed, failed, expired, or reversed.
The hub polls this every 2.5 seconds for up to 2 minutes. Unknown id: 404, code 2706.
Hosted checkout: read
GET /checkouts/{id}Returns the payer view plus the payee. It omits reference, the receipt, and the attempt phone.
json
{
"ok": true,
"description": "Checkout",
"data": {
"checkout": {
"id": "cs_ke_3f9a1c7e5b2d4a8e6c0b91d7",
"status": "open",
"amount": "150000",
"currency": "KES",
"description": "2× Latte, 1× Mandazi",
"phone": null,
"intent_id": null,
"return_url": "https://yourshop.example/paid",
"redirect_url": null,
"paybill": {
"business_number": "…",
"account_number": "CS12345678"
},
"card": null,
"expires_at": "2026-09-30T09:30:00.000Z"
},
"payee": { "paybill": null }
}
}| Field | |
|---|---|
phone | The number you sent at create, or null. The payer can change it |
intent_id | Latest in-flight or completed attempt, or null |
redirect_url | Set only when status is completed. Your return_url plus checkout, reference, and status |
paybill | Kenya Pay Bill numbers for this checkout, or null. account_number is CS plus 8 digits. See M-Pesa menu |
card | { "fee": "…", "total": "…" } when card is on, else null. fee is added for the payer. You receive amount |
payee.amount | The checkout amount, as a string. The payer cannot change it |
payee.paybill | Always null on this route. Menu pay uses checkout.paybill |
404 code 2783: unknown id, or the merchant cannot be paid on this host. 503 code 2752: the ledger did not answer.
Hosted checkout: pay by mobile money
POST /checkouts/{id}/payIdempotency-Key is required. Body: { "phone": "254712345678" }. rail is optional.
| HTTP | Code | |
|---|---|---|
202 | New prompt. description is Checkout initiated | |
200 | An attempt is already in flight. description is Checkout already in progress | |
200 | Idempotency replay of a new prompt. description stays Checkout initiated. message is Duplicate request — returning existing intent | |
409 | 2785 | Already paid |
409 | 2784 | Expired, and nothing is in flight |
404 | 2783 | Unknown checkout |
The body matches Pay a handle or link. Poll GET /intents/{id}.
Hosted checkout: pay by card
POST /checkouts/{id}/cardOnly when checkout.card is not null. Idempotency-Key is required. Optional body fields: payer_name, payer_email, phone.
A new session returns 202. A replay returns 200. description is Card payment initiated. The body has amount, fee, total, and the card session fields.
| HTTP | Code | When |
|---|---|---|
409 | 2786 | Another payment for this checkout is in progress |
422 | 2771 | Card is off for this checkout |
403 | 2776 | The page origin is not allowed |
A profile
GET https://auth-ke-staging.salimia.me/v1/auth/public/profile/{handle}This route is on the auth host for that country (auth-ke-staging or auth-ug-staging), not on the payments host. It returns the bio, theme, cover, and social links for the payer page. 404 when the handle does not exist.