Amounts, numbers & ids
The formats every endpoint shares.
Amounts
Every amount is an integer number of minor units, sent and returned as a JSON string. Strings keep large figures exact in every language, where a JSON number might be rounded.
| Currency | Minor unit | Decimals | "amount": "50000" |
|---|---|---|---|
UGX | 1 shilling | 0 | USh 50,000 |
KES | 1/100 of a unit | 2 | KSh 500.00 |
GHS | 1/100 of a unit | 2 | GH₵ 500.00 |
const DECIMALS = { UGX: 0, KES: 2, GHS: 2 };
// "500.50" KES -> "50050"
function toMinor(display, currency) {
const [whole, frac = ""] = String(display).split(".");
const d = DECIMALS[currency];
return (BigInt(whole) * 10n ** BigInt(d) + BigInt((frac + "0".repeat(d)).slice(0, d) || "0")).toString();
}An amount must be greater than zero and match ^\d{1,19}$: digits only, no sign, no decimal point, no spaces. Every amount travels with its currency.
Phone numbers
Send the wallet's number in the msisdn field. International format is safest: +256772123456. SkanPay also accepts 00256772123456, and local forms such as 0772123456 or 772123456, which are read against your account's country (or the country you send). Spaces, dashes, dots and brackets are ignored.
Resolution happens in this order:
- The number is normalised to international (E.164) form.
- The country comes from the dialing code.
- The number must have the right length: 9 digits after the country code in all three countries.
- The network comes from the number's prefix, or from the
networkyou send.
If the prefix is not one we know, or the range is shared or ported, the request fails with network_required. SkanPay never guesses a network, because a wrong guess sends money to the wrong rail. Ask the customer which network they use and pass network: "MTN" or network: "AIRTEL". A network you pass always wins over the prefix table.
Recognised prefixes
| Country | MTN | Airtel |
|---|---|---|
| Uganda (+256) | 077, 078, 076, 039 | 070, 075, 074, 020 |
| Kenya (+254) | — | 073, 078, 0100, 0101, 0102 |
| Ghana (+233) | 024, 025, 053, 054, 055, 059 | 026, 027, 056, 057 |
Numbers are never returned in full. Every object carries msisdn_masked, for example +2567****456, so a leaked API response or log line does not expose your customers.
Your reference
reference is your own id for the payment, usually the order id. It is returned unchanged on the object and in every webhook, and GET /v1/charges?reference=… finds it again. It is 1 to 128 characters from A–Z a–z 0–9 . _ : -.
Metadata
Charges, payouts, deposits and withdrawals accept an optional metadata object for your own data: up to 20 keys, each key at most 64 characters, each value a string (up to 512 characters), a number, a boolean or null. It is returned on the object and in webhooks. Don't put secrets or customer ID numbers in it.
Object ids
Ids are opaque strings with a type prefix. They sort by creation time, which is what makes cursor pagination work. Store them as strings of up to 64 characters.
| Prefix | Object |
|---|---|
chg_ | Charge (cash-in) |
pyt_ | Payout (cash-out to any wallet) |
dep_ | Deposit (top-up of your balance) |
wdl_ | Withdrawal (cash-out to your own wallet) |
ref_ | Refund |
wal_ | Payout wallet |
evt_ | Webhook event, and status events in a timeline |
Timestamps
created_at and updated_at are ISO 8601 in UTC, for example 2026-09-28T09:30:08.000Z. Filters such as created_after take the same format.
Request ids
Every response carries an X-Request-Id header (req_…), and every error body repeats it as request_id. Log it with your own order; it is the fastest way for support to find a request.
Rate limits
| Scope | Limit |
|---|---|
| All requests, per API key | 300 per minute |
| Creating payouts and withdrawals | 60 per minute |
Past the limit you get 429 rate_limit_exceeded. Back off and retry. An integration that uses webhooks instead of polling stays far below these limits.
Request format
- HTTPS only. Request and response bodies are JSON (
Content-Type: application/json). - Unknown body fields are rejected, not ignored, so typos surface immediately.
- Bodies are limited to 1 MB.