Error codes
Every failure is a code, never a sentence — you render it, in your own checkout, in your own language. The body is always the same shape:
json
{ "error": { "code": "BAD_AMOUNT", "doc": "https://docs.pamopay.co/errors/BAD_AMOUNT", "request_id": "req_…" } }FORBIDDEN_SCOPE adds scope, the one thing you need to fix it. Nothing else ever adds a field. Every code links to its own page, and this table is rendered from the API's own catalogue: it cannot answer a code that is not here.
Retry says whether the same request may be sent again unchanged: same request means yes, with the same Idempotency-Key; fix request means something must change first; never means a person has to act.
| Code | HTTP | What happened | Retry |
|---|---|---|---|
BAD_KEY | 401 | The Authorization header was missing, was not Bearer <key>, or carried a key we did not issue. | fix request |
KEY_REVOKED | 401 | This key was retired from the dashboard and has now stopped. | never |
KEY_EXPIRED | 401 | The key reached the lifetime it was given when it was made. | never |
FORBIDDEN_SCOPE | 403 | The key is valid and this route is not among the things it was made to do. scope in the error body names what was needed. | never |
MERCHANT_NOT_LIVE | 403 | A live key for a business that has not gone live, or has been suspended. This is about the account, not the key. | never |
RATE_LIMITED | 429 | This key made more calls in the current minute than it is allowed. Retry-After says how many seconds until the window resets. | same request |
USE_SANDBOX_URL | 400 | A sk_test_ key was sent to the live base URL. Test keys are served by the sandbox at its own URL. | fix request |
LIVE_KEY_ON_SANDBOX | 400 | A sk_live_ key was sent to the sandbox base URL, which never moves money and will not pretend to. | fix request |
BAD_AMOUNT | 400 | amount_minor is not an integer, is not a whole number of shillings (a multiple of 100), or is outside TZS 100–20,000,000. | fix request |
BAD_CURRENCY | 400 | Only TZS is supported. | fix request |
BAD_RAIL | 400 | rail is not one of mpesa, airtel, tigo, halopesa, azampesa. | fix request |
BAD_MSISDN | 400 | msisdn is not a Tanzanian mobile number. | fix request |
BAD_NUMBER | 400 | The number to notify about a bill is not a Tanzanian mobile number. | fix request |
AMOUNT_TOO_SMALL | 400 | After the fee there would be nothing left for the business. | fix request |
BAD_REQUEST | 400 | A field or query parameter is the wrong shape, or (413) the body is over 64 kB. | fix request |
BAD_CUSTOMER_REF | 400 | A bill needs customer_ref — the student number, the tenant, the invoice — and it was missing or over 64 characters. | fix request |
BAD_EXPIRY | 400 | expires_at is not an ISO-8601 instant, or is already in the past. | fix request |
NO_REGISTERED_WEBSITE | 400 | success_url was given, and this business has no website registered, so there is nowhere a return URL is allowed to go. | fix request |
BAD_RETURN_URL | 400 | success_url is not https, or is not on the website registered for this business. A return URL on a payments domain is an open redirect otherwise. | fix request |
BAD_URL | 400 | url for a webhook endpoint is not https, has no host, carries credentials, or is over 500 characters. | fix request |
BAD_TILL | 400 | till is not six digits. | fix request |
UNKNOWN_TILL | 404 | No open till with that number belongs to this business. | fix request |
TILL_CLOSED | 409 | That till has been closed and cannot ring up a sale. | never |
NOT_FOUND | 404 | No such object for this business. An object belonging to another business is also NOT_FOUND — telling a caller which references exist would be a way to enumerate them. | fix request |
ALREADY_QUEUED | 409 | A replay was asked for on an event that is already queued or in flight. What you wanted is going to happen. | never |
IDEMPOTENCY_REQUIRED | 400 | This request takes money and needs an Idempotency-Key header: 8 to 128 characters of letters, digits, hyphen or underscore. It was missing or the wrong shape. | fix request |
METHOD_NOT_ALLOWED | 405 | The path exists and does not answer this verb. | fix request |
IDEMPOTENT_MISMATCH | 409 | The same Idempotency-Key was sent with a different request. Nothing was created; the first request under that key stands. | fix request |
LEDGER_UNAVAILABLE | 503 | The charge could not be opened — the ledger did not answer, or no tariff covers this business on this rail. Nothing was taken. | same request |
By status
- 400 — the request. Change it and send it again.
- 401 — the key: unrecognised, retired or expired.
- 403 — a real key that may not do this: outside its scopes, or a live key on a business that is not live.
- 404 — no such object for this business. Another business's object is 404 too, never 403.
- 405 — right path, wrong verb.
- 409 — your intent is already true, or contradicts an earlier request under the same key.
- 413 — the body is over 64 kB.
- 429 — too many calls this minute.
Retry-Aftersays how long. - 503 — ours. Nothing was taken; retry with the same key.