Skip to content

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.

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
nameThe address in the URL. On a link this differs from username
usernameAccount owner
title, amountLink title and suggested amount, minor units, as a string. null on a handle or an open link
served_herefalse 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, maxHow this host collects. Amounts are minor units
paybillM-Pesa Pay Bill business number, or null. The account number the payer types is name
acceptingThe 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.

POST /checkout

Sends a prompt to the payer phone. Idempotency-Key is required (1 to 128 characters).

FieldRequired
handleYesAddress to pay
amountYesMinor units
phoneYesPayer number. 9 to 15 digits, country code, no +
railNoDefault 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
phoneThe number you sent at create, or null. The payer can change it
intent_idLatest in-flight or completed attempt, or null
redirect_urlSet only when status is completed. Your return_url plus checkout, reference, and status
paybillKenya 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.amountThe checkout amount, as a string. The payer cannot change it
payee.paybillAlways 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}/pay

Idempotency-Key is required. Body: { "phone": "254712345678" }. rail is optional.

HTTPCode
202New prompt. description is Checkout initiated
200An attempt is already in flight. description is Checkout already in progress
200Idempotency replay of a new prompt. description stays Checkout initiated. message is Duplicate request — returning existing intent
4092785Already paid
4092784Expired, and nothing is in flight
4042783Unknown checkout

The body matches Pay a handle or link. Poll GET /intents/{id}.

Hosted checkout: pay by card ​

POST /checkouts/{id}/card

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

HTTPCodeWhen
4092786Another payment for this checkout is in progress
4222771Card is off for this checkout
4032776The 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.

Staging documentation. Staging charges real money. See Environments.