Skip to content

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.

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.

Three rules, in order.

  1. 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.
  2. On HTTP 200, ignore the status entirely and look at the body.
  3. Test error?.code, never error itself. A successful GetTransactionStatus returns an error object 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.

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.

Every code observed against the sandbox.

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
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
code message
invalid_api_credentials_provided Invalid Access Token
invalid_api_credentials_provided Invalid or Missing Credentials Provided

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.