Errors
Pesapal’s API does not use HTTP status codes to signal failure. A rejected payment, wrong credentials, and an unsupported currency all arrive as HTTP 200, with the failure described only in the body.
Real HTTP status codes do appear, but only for requests rejected before they
reach the application: 401, 404, 405, 409, 411 and 415. Those
responses have a different shape again, so a client has to handle both layers.
Response Shapes
Section titled “Response Shapes”There are eight, and they are not interchangeable.
| # | When | Shape |
|---|---|---|
| 1 | Successful auth or order | {...data, "error": null, "status", "message"} |
| 2 | Most application errors | {"error": {error_type, code, message}, "status"} |
| 3 | HTTP 404, 405, 415 | {"message": "<plain text>"} |
| 4 | HTTP 401, 409 | {"message": "<JSON encoded as a string>"} |
| 5 | GetIpnList |
A bare [], no envelope at all |
| 6 | CancelOrder, RefundRequest |
{"status", "message"}, no error key |
| 7 | POST to a GET endpoint (411) | HTML, not JSON |
| 8 | GetTransactionStatus success |
error is an object of nulls, not null |
Shapes 3 and 4 are structurally identical. The only way to tell them apart is to
try parsing message as JSON and see whether it succeeds.
Detecting Success Reliably
Section titled “Detecting Success Reliably”Three rules, in order.
- Check the HTTP status for framework failures first. Anything other than 200 means the request never reached the application. The body may be plain text, escaped JSON, or HTML.
- On HTTP 200, ignore the status entirely and look at the body.
- Test
error?.code, nevererroritself. A successfulGetTransactionStatusreturns anerrorobject whose fields are all null, which is truthy.
async function call(url, options) { const res = await fetch(url, options); const text = await res.text();
// Shape 7: an IIS error page, not JSON. if (text.startsWith('<')) { throw new Error(`HTTP ${res.status}: non-JSON response`); }
const body = JSON.parse(text);
// Shapes 3 and 4: a bare message, sometimes containing escaped JSON. if (!res.ok) { let detail = body.message; try { detail = JSON.parse(body.message).error.message; } catch { // Shape 3, message was plain text after all. } throw new Error(`HTTP ${res.status}: ${detail}`); }
// Shape 2, and shape 8's object of nulls, which must not count as an error. if (body.error?.code) { throw new Error(`${body.error.code}: ${body.error.message}`); }
return body;}Shapes 5 and 6 still need per-endpoint handling. GetIpnList returns a bare
array with no envelope, and CancelOrder and RefundRequest signal failure
through status: "500" with no error key at all.
The Error Object
Section titled “The Error Object”Shape 2, the common case:
{ "error": { "error_type": "invalid_request_error", "code": "invalid_amount_provided", "message": "Invalid amount provided,please pass valid amount value" }, "status": "500"}| Field | Notes |
|---|---|
error_type |
api_error, invalid_request_error or authentication_error. |
| code | Machine-readable. Mostly snake_case, but see below. |
| message | Human-readable. Sometimes the only thing distinguishing two different failures. |
| status | A quoted string, usually “500”. Sometimes null. Never matches the HTTP status. |
Error Codes
Section titled “Error Codes”Every code observed against the sandbox.
invalid_request_error
Section titled “invalid_request_error”code |
message |
|---|---|
invalid_api_request_parameters |
Consumer Key is required|Consumer Secret is required |
invalid_api_request_parameters |
Currency code is required |
invalid_api_request_parameters |
Invalid currency code provided.Please provide currency in ISO Format |
invalid_api_request_parameters |
Invalid Order Reference length.Required Maximum length is 150 characters |
invalid_api_request_parameters |
Invalid API request. |
invalid_amount_provided |
Invalid amount provided,please pass valid amount value |
invalid_order_tracking_id |
Invalid order id provided.Please check your id and try again |
api_error
Section titled “api_error”code |
message |
|---|---|
invalid_consumer_key_or_secret_provided |
Invalid Access Credentials provided |
invalid_url |
The specified url provided is invalid |
invalid_ipn_notification_type |
Invalid IPN Notification type.Accepted values are POST or GET |
InvalidIpnId |
The specified IPN ID is invalid |
invalid_merchant_reference |
Invalid merchant reference provided |
invalid_payment_currency |
The provided currency specified is not supported |
missing_mandatory_billing_address |
Please provide billing details information on your API request. |
payment_details_not_found |
Payment details not found |
payment_details_not_found |
Pending Payment |
invalid_api_request_parameters |
URL is required |
invalid_api_request_parameters |
Invalid length.Maximum 4 characters |
invalid_api_request_parameters |
Invalid subscription plan details provided. |
general_system_decline_error |
Request Declined.Too many requests |
general_system_decline_error |
Unable to process your order at the moment.Please try again later |
authentication_error
Section titled “authentication_error”code |
message |
|---|---|
invalid_api_credentials_provided |
Invalid Access Token |
invalid_api_credentials_provided |
Invalid or Missing Credentials Provided |
Patterns Worth Knowing
Section titled “Patterns Worth Knowing”Validation runs in two stages, and the first gives worse errors. Length and shape are checked before value. The more wrong your input, the less useful the message.
| Input | Message |
|---|---|
ipn_notification_type: "CARRIER_PIGEON" |
Invalid length.Maximum 4 characters |
ipn_notification_type: "PUT" |
Invalid IPN Notification type.Accepted values are POST or GET |
A value that happens to be the right length gets a helpful error. A longer one gets a message about length that never mentions the real problem.
general_system_decline_error is a catch-all. It covers rate limiting
(Request Declined.Too many requests) and validation failures the API declines
to explain (Unable to process your order at the moment). You cannot branch on
the code, only on the message.
Unknown fields are silently ignored. A misspelled field name such as
callback_uri produces no error. It is discarded, and the failure appears later
as behaviour that never happens.
Values are case insensitive. get, kes and monthly are all accepted and
normalised. Path segments too: GetIpnList and GetIPNList both route.