Error Codes
This page documents all error codes and responses you may encounter when using the ZoPay API. Understanding these errors will help you build robust integrations.
Error Response Format
All error responses follow a consistent JSON format:
1{
2 "error": "Error Type",
3 "message": "Human-readable description of the error",
4 "code": "MACHINE_READABLE_CODE"
5}HTTP Status Codes
Client Errors (4xx)
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_ERROR | Invalid request parameters or body |
400 | QUOTE_EXPIRED | Quote has expired (15 minute validity window) |
400 | QUOTE_ALREADY_USED | Quote has already been used for a transaction |
400 | INVALID_PHONE | Phone number format is invalid |
400 | INVALID_AMOUNT | Amount is below minimum or above maximum |
401 | AUTH_ERROR | Authentication failed (invalid API key or signature) |
401 | INVALID_SIGNATURE | HMAC signature verification failed |
401 | TIMESTAMP_EXPIRED | Request timestamp is too old (replay attack prevention) |
401 | NONCE_REUSED | Nonce has been used before within the 10-minute window |
403 | ACCESS_FORBIDDEN | IP/domain not in allowlist or merchant not activated |
403 | ENVIRONMENT_MISMATCH | Using sandbox key on production or vice versa |
404 | NOT_FOUND | Resource not found (transaction, quote, payout, etc.) |
402 | INSUFFICIENT_BALANCE | Merchant wallet balance is too low for this transaction |
409 | DUPLICATE_IDEMPOTENCY_KEY | Transaction with same idempotency key already exists |
429 | RATE_LIMIT_EXCEEDED | Too many requests (100/min default limit) |
Server Errors (5xx)
| Status | Code | Description |
|---|---|---|
500 | INTERNAL_ERROR | Unexpected server error - contact support if persistent |
502 | GATEWAY_ERROR | Mobile money provider is unreachable |
503 | SERVICE_UNAVAILABLE | Service temporarily unavailable - retry later |
504 | GATEWAY_TIMEOUT | Mobile money provider request timed out |
Authentication Errors
Authentication errors occur when your request headers are invalid or missing.
Invalid API Key
1{
2 "error": "Unauthorized",
3 "message": "Invalid API key",
4 "code": "AUTH_ERROR"
5}Fix: Verify your x-zo-key header contains the correct API key from your dashboard.
Invalid Signature
1{
2 "error": "Unauthorized",
3 "message": "Invalid signature",
4 "code": "INVALID_SIGNATURE"
5}Fix: Verify your signature generation. See the Authentication page for correct implementation.
Expired Timestamp
1{
2 "error": "Unauthorized",
3 "message": "Request timestamp expired",
4 "code": "TIMESTAMP_EXPIRED"
5}Fix: Ensure your server clock is synchronized. Timestamps must be within ±5 minutes of server time.
Transaction Errors
Quote Expired
1{
2 "error": "Bad Request",
3 "message": "Quote has expired. Please create a new quote.",
4 "code": "QUOTE_EXPIRED"
5}Fix: Quotes are valid for 15 minutes. Create a new quote before executing the transaction.
Insufficient Balance
1{
2 "error": "Payment Required",
3 "message": "Insufficient wallet balance for this transaction",
4 "code": "INSUFFICIENT_BALANCE"
5}Fix: Top up your merchant wallet before retrying the disbursement.
Duplicate Idempotency Key
1{
2 "error": "Conflict",
3 "message": "Transaction with this idempotency key already exists",
4 "code": "DUPLICATE_IDEMPOTENCY_KEY"
5}Fix: Use a unique idempotency key for each new transaction. If retrying, the original transaction result will be returned.
Rate Limiting
When you exceed the rate limit, you will receive a 429 response:
1{
2 "error": "Too Many Requests",
3 "message": "Rate limit exceeded. Retry after 30 seconds.",
4 "code": "RATE_LIMIT_EXCEEDED"
5}The response includes a retry_after header indicating how many seconds to wait before retrying.
Best Practice: Implement exponential backoff when handling rate limit errors. Start with the retry_after value, and double the wait time on subsequent 429 responses.
Gateway-Specific Errors
These errors occur when the mobile money provider cannot process the transaction:
| Error | Description |
|---|---|
PAYER_NOT_FOUND | Phone number is not registered with the gateway |
PAYER_INSUFFICIENT_FUNDS | Customer does not have enough balance |
TRANSACTION_DECLINED | Customer declined the payment prompt |
GATEWAY_TIMEOUT | Gateway did not respond in time |
GATEWAY_UNAVAILABLE | Gateway is temporarily offline |
AMOUNT_TOO_LOW | Amount is below the gateway minimum |
AMOUNT_TOO_HIGH | Amount exceeds the gateway maximum |
Error Handling Best Practices
- Always check HTTP status codes: Use the status code to determine the error category before parsing the body
- Log error details: Store the full error response for debugging
- Implement retries for 5xx: Server errors are often transient - retry with exponential backoff
- Never retry 4xx errors: Client errors require fixing the request (except 429)
- Handle gateway errors gracefully: Show user-friendly messages when mobile money operations fail
- Use webhooks for async results: Don't rely only on synchronous responses for final transaction status
Example: Error Handler
1async function makeZoPayRequest(url, options) {
2 const response = await fetch(url, options);
3
4 if (!response.ok) {
5 const error = await response.json();
6
7 switch (response.status) {
8 case 401:
9 // Authentication error - check API keys and signature
10 throw new Error(`Auth error: ${error.message}`);
11 case 402:
12 // Insufficient funds
13 throw new Error('Insufficient wallet balance');
14 case 429:
15 // Rate limited - wait and retry
16 const retryAfter = response.headers.get('retry_after') || 30;
17 await new Promise(r => setTimeout(r, retryAfter * 1000));
18 return makeZoPayRequest(url, options); // Retry
19 case 500:
20 case 502:
21 case 503:
22 // Server error - retry with backoff
23 throw new Error(`Server error: ${error.message}`);
24 default:
25 throw new Error(`API error [${error.code}]: ${error.message}`);
26 }
27 }
28
29 return response.json();
30}