Errors
How Scan to Pay returns errors — HTTP status codes, the platform-specific 4xx error catalogue, and how to handle each.
Scan to Pay returns errors using standard HTTP status codes plus an extended set of 4xx codes in the 431–481 range for platform-specific failures. The error body, when present, contains a human-readable explanation of what went wrong.
This page is the canonical reference for every error code the API can return.
For transaction outcomes, see Transaction states. Errors here are protocol-level failures (your request couldn't be processed). Once a transaction reaches the platform successfully, its outcome is communicated viaTxStateorNotificationStatus, not HTTP error codes.
Standard HTTP statuses
| Status | Meaning | Typical cause |
|---|---|---|
200 OK | Success | Request processed and returned data |
400 Bad Request | Malformed request | Missing required field, invalid JSON, validation failure. Inspect the response body for the specific error |
401 Unauthorized | Authentication failed | Wrong username or password, missing Authorization header, expired JWT |
403 Forbidden | Authenticated but not permitted | Your account doesn't have the role required for this path — see Authentication for the role / path mapping |
404 Not Found | Resource doesn't exist | Code, transaction, or merchant not found at the requested ID |
406 Not Acceptable | Operation not allowed in current state | Most commonly: calling queryRef on a merchant that has webhook delivery configured |
500 Internal Server Error | Platform error | Something failed inside Scan to Pay. Retry with exponential backoff. If it persists, raise a support ticket |
Scan to Pay extended error codes
Every Scan to Pay error returns one of the codes below in the 431–481 range. The HTTP status code identifies the error; the response body provides additional detail.
Code-related errors (431–456)
| Code | Name | Meaning |
|---|---|---|
431 | CODE_NOT_FOUND | The referenced code doesn't exist. Check the code value. |
432 | INVALID_AMOUNT | The amount is zero, negative, exceeds the maximum, or doesn't match the code amount on a fixed-amount code. |
432 | INSUFFICIENT_BALANCE | Wallet has insufficient balance to cover the amount (wallet flows only). |
433 | INVALID_NETWORK | The network specified in a Network Purchase request isn't recognised. |
434 | CLIENT_SUSPENDED | The customer's account has been suspended. |
436 | INVALID_TRANSACTION_REF | The transaction reference doesn't match any known transaction. |
438 | CARD_ERROR | Card validation or tokenisation failed. Body includes specific reason. |
439 | INVALID_CODE | The code format is wrong. |
440 | VALIDATION_ERROR | Generic field validation failed. Body includes the specific field. |
441 | INVALID_MERCHANT | The merchant ID is unknown or inactive. |
442 | INVALID_CLIENT | The client (cardholder) ID is unknown. |
443 | CODE_LOCKED | The code is currently locked (being paid in another flow). Wait or use a different code. |
444 | NOTHING_TO_REVERSE | No active transaction to reverse. |
445 | INVALID_CLIENT_CARD | The card reference is invalid for this client. |
446 | INVALID_PASSWORD | Profile password validation failed. |
447 | INVALID_BASKET | The cart items provided don't validate against the rules (e.g. AIRTIME basket without destination). |
448 | CLIENT_LIMIT_EXCEEDED | The customer hit their daily/monthly limit. |
449 | CARD_HASH_FAILED | The card-to-MSISDN hash check failed (anti-fraud). |
450 | INVALID_PAYMENT | The payment type isn't supported by this merchant or for this card. |
451 | FRAUD_DETECTION | Transaction blocked by fraud detection. |
452 | CODE_RESERVED | Code is in RESERVED state and can't be used. |
453 | CODE_DELETED | Code has been deleted. |
454 | CODE_ACTIVE | Operation not valid on an active code. |
455 | CODE_USED | Code has already been used (use-once code). |
456 | CODE_BLOCKED | Code is blocked. Unblock first, or use a different code. |
Merchant and transaction errors (460–481)
| Code | Name | Meaning |
|---|---|---|
460 | MERCHANT_NOT_FOUND | The merchant doesn't exist or isn't reachable. |
461 | INVALID_REQUEST | The request body or query string is malformed in a way that doesn't match a more specific validation rule. |
462 | CNP_VALIDATION_STEP_UP_REQUIRED | Card-not-present transaction requires additional authentication (step-up to 3DS). |
463 | INVALID_TOKEN | The provided token is invalid or expired. |
464 | FAIL_OR_RETRY_TX_BANK_DECLINE | The bank declined. Body includes the bank response code — see ISO response codes. |
465 | MISSING_MSISDN | An MSISDN is required for this operation but wasn't provided. |
466 | INVALID_OR_MISSING_PAYMANT_TYPE | Payment type missing or unrecognised. |
467 | NOT_ALLOW_PAYMENT_TYPE | The merchant doesn't accept this payment type (typically CNP). |
468 | WIG_CNP_PURCHASE_FAIL_CARD_EXPIRY_BIN_NOT_WHIELIST | USSD CNP purchase failed: card expiry or BIN not whitelisted. |
469 | WIG_3DS_PURCHASE_FAIL_CARD_EXPIRY_BIN_NOT_WHIELIST | USSD 3DS purchase failed: card expiry or BIN not whitelisted. |
470 | INVALID_QR_COLOR_CODE | QR colorCode query param must be 0–3. |
471 | EXTERNAL_API_ERROR | A downstream service (acquirer, BIN lookup, etc.) returned an error. |
472 | MERCHANT_WHITELIST_FAILURE | The merchant whitelist check failed for this card/wallet. |
473 | INSUFFICIENT_INFO_FOR_MERCHANT_WHITELIST | Not enough data to evaluate the whitelist rule. |
474 | INVALID_VOUCHER_CODE | The voucher code is invalid. |
475 | VOUCHER_ERROR | A voucher purchase failed for another reason. |
476 | BIN_NOT_FOUND | The card BIN isn't in the BIN tables. |
477 | CCPI_ERROR | A recurring (CCPI) purchase failed. |
478 | PROXY_ERROR | An upstream proxy returned an error. |
479 | CUSTOM_ERROR | A custom error specified by the merchant configuration. Body explains. |
480 | PAYSHAP_ERROR | A PayShap rapid-payments transaction failed. |
481 | INVALID_CARD_EXPIRY | The card expiry date is invalid or in the past. |
Platform errors (5xx range)
These are platform-side errors rather than client mistakes. They're worth distinguishing because the right response is "retry" rather than "fix your request."
| Code | Name | Meaning |
|---|---|---|
500 | INTERNAL_SERVER_ERROR | Generic platform error. Body usually contains a request ID. Retry with backoff. |
512 | SYSTEM_ERROR | An internal Scan to Pay system error. Investigate via support if persistent. |
513 | SECURITY_VIOLATION | An operation was attempted that the platform considers a security violation (e.g. one merchant trying to access another's transaction). |
514 | UNABLE_TO_REVERSE | The platform attempted to reverse but couldn't (outside the window, already reversed, etc.). |
515 | UNABLE_TO_REFUND | The platform attempted to refund but couldn't (different card type, already refunded, outside acquirer window). |
516 | INTERRUPTED | The operation was interrupted mid-processing. Check the transaction state via getTransactionState before retrying. |
Error response format
When the platform returns a 4xx or 5xx, the response body usually contains a JSON object with the error code and a message:
{
"status": "FAILED",
"errorCode": 432,
"errorMessage": "Invalid Amount: amount must be greater than zero"
}Older endpoints return a simpler shape:
{
"error": "Validation error: merchantReference is required"
}Always check the HTTP status code first, then the body for detail. The status code is the source of truth; the body is human help.
Codes that return noerrorMessage:462 CNP_VALIDATION_STEP_UP_REQUIRED,463 INVALID_TOKEN,472 MERCHANT_WHITELIST_FAILURE,479 CUSTOM_ERROR, and480 PAYSHAP_ERRORreturn only the HTTP status anderrorCode— there is no message string in the response body. Identify these byerrorCodealone.
Handling errors in your integration
A simple decision tree for what to do with each class of error:
| Class | Examples | What to do |
|---|---|---|
| Authentication | 401 | Verify credentials are correct and not rotated. Refresh JWT if using Bearer. |
| Authorisation | 403 | Wrong role for the endpoint. Check Authentication for the path/role table. |
| Validation | 400, 432, 440, 447 | Fix your request. Do not retry the same payload — it'll fail again. |
| State conflict | 406, 443, 444, 452–456 | The resource isn't in a state where the operation is allowed. Inspect with query first, take corrective action. |
| Bank/Network | 434, 438, 448, 464, 468, 469, 481 | Customer-side problem. Display a friendly message and let the customer retry with different data or a different card. |
| Card data | 438, 445, 449, 476, 481 | Card-specific failure. Customer should try a different card. |
| Fraud / risk | 448, 451, 472, 473 | Don't retry. The platform blocked the transaction deliberately. |
| Transient platform | 500, 512, 516 | Retry with exponential backoff. After 3 attempts, escalate to support with the request ID. |
| Code lifecycle | 431, 443, 452–456 | Inspect the code state and create a fresh code if needed. |
Best practices
- Always log the response body, not just the HTTP status. The body is where the specific reason lives.
- Map error codes to customer-facing copy in your UI. Never show
CODE_LOCKEDorCARD_HASH_FAILEDto an end user. See ISO response codes for suggested customer messages. - Distinguish retriable from non-retriable. 5xx and transient 4xx (
516 INTERRUPTED) → retry. Validation 4xx → do not retry the same request. - Capture request IDs if the response includes one — support tickets are much faster to resolve with a request ID attached.
- Use idempotent retries. When you retry, use the same
merchantReferenceso the platform recognises it as the same logical attempt. See Idempotency. - Don't catch and swallow. Especially on webhook handlers, surface errors prominently so on-call sees them.
What's next
- Map bank decline codes (
464) to customer-facing copy → ISO response codes - Understand transaction states vs HTTP errors → Transaction states
- Retry safely with the same reference → Idempotency
- Authentication failure modes → Authentication
- Webhook delivery failure modes → Webhooks
Updated about 20 hours ago
