Error Handling Overview
The JPay Africa API uses standard HTTP status codes to indicate success or failure. This guide explains how to handle errors effectively in your integration.
Error Response Format
All error responses follow a consistent JSON structure:
{
"detail": "Human-readable error message"
}
HTTP Status Codes
2xx Success
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request successful, response contains data |
| 201 | Created | Resource successfully created |
| 204 | No Content | Request successful, no content to return |
4xx Client Errors
| Code | Meaning | Description |
|---|---|---|
| 400 | Bad Request | Invalid request parameters or malformed data |
| 401 | Unauthorized | Authentication failed or token invalid/expired |
| 403 | Forbidden | Authenticated but not authorized for this resource |
| 404 | Not Found | Requested resource does not exist |
| 429 | Too Many Requests | Rate limit exceeded |
5xx Server Errors
| Code | Meaning | Description |
|---|---|---|
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Temporary server connectivity issue |
| 503 | Service Unavailable | Server temporarily unavailable |
Common Error Scenarios
400 Bad Request
This error indicates an issue with your request data. Common causes:
Invalid Phone Number
{
"detail": "Invalid phone number"
}
Solution: Ensure phone number is in E.164 format (e.g., +254712345678)
Invalid Amount
{
"detail": "Invalid amount"
}
Solution: Ensure amount is a decimal string with exactly 2 decimal places (e.g., "1000.00")
Missing Required Field
{
"detail": "This field is required"
}
Solution: Include all required fields in your request
Invalid JSON
{
"detail": "JSON parse error"
}
Solution: Ensure your request body is valid JSON
401 Unauthorized
Authentication failed. Common causes:
Invalid Token
{
"detail": "Invalid or expired token"
}
Solution: Get a new token using app authentication or merchant login
Missing Authorization Header
{
"detail": "Authentication credentials were not provided"
}
Solution: Include Authorization: Bearer YOUR_TOKEN header
Malformed Authorization Header
{
"detail": "Invalid token format"
}
Solution: Use format Bearer YOUR_TOKEN (with space)
403 Forbidden
Authentication succeeded but not authorized. Common causes:
Merchant Not Approved
{
"detail": "Your merchant profile is not approved. Please ensure your profile has been verified before initiating payments."
}
Solution: Complete merchant verification in dashboard
App Inactive
{
"detail": "App is not active"
}
Solution: Activate your app in the merchant dashboard
Product Not Enabled
{
"detail": "App does not have collections product enabled"
}
Solution: Enable the required product for your app
Insufficient Balance
{
"detail": "Insufficient balance in payout wallet"
}
Solution: Fund your payout wallet or wait for settlement
404 Not Found
Resource doesn't exist:
App Not Found
{
"detail": "App not found"
}
Solution: Verify your app code is correct
Endpoint Not Found
{
"detail": "Not found"
}
Solution: Verify the endpoint URL is correct
429 Too Many Requests
Rate limit exceeded:
{
"detail": "Request rate limit exceeded"
}
Rate Limits:
- Standard: 60 requests per minute
- Burst: 100 requests (within a 10-second window)
Solution: Implement exponential backoff retry logic
500 Internal Server Error
Server-side error:
{
"detail": "Internal server error"
}
Solution: Retry with exponential backoff. If error persists, contact support.
Error Handling Best Practices
1. Check Status Code First
Always check the HTTP status code before processing the response:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
data = response.json()
# Process successful response
elif response.status_code == 400:
error = response.json()
print(f"Bad request: {error['detail']}")
elif response.status_code == 401:
# Refresh token or re-authenticate
pass
# ... handle other status codes
2. Implement Retry Logic
Retry failed requests with exponential backoff:
import time
import requests
def api_request_with_retry(method, url, headers, json=None, max_retries=3):
for attempt in range(max_retries):
try:
if method == 'GET':
response = requests.get(url, headers=headers, timeout=10)
elif method == 'POST':
response = requests.post(url, headers=headers, json=json, timeout=10)
else:
raise ValueError(f"Unsupported method: {method}")
# Don't retry on client errors (4xx) except 429
if 400 <= response.status_code < 500 and response.status_code != 429:
return response
# Retry on 429, 5xx, and connection errors
if response.status_code == 429 or response.status_code >= 500:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
print(f"Rate limited or server error. Waiting {wait_time}s before retry...")
time.sleep(wait_time)
continue
return response
except requests.RequestException as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt
print(f"Request failed: {e}. Retrying in {wait_time}s...")
time.sleep(wait_time)
else:
raise
return None
3. Log Errors for Debugging
Always log errors for troubleshooting:
import logging
logger = logging.getLogger(__name__)
def handle_api_error(response):
error_data = response.json()
logger.error(
f"API Error {response.status_code}",
extra={
'status_code': response.status_code,
'error_detail': error_data.get('detail'),
'request_url': response.request.url,
'request_method': response.request.method,
'timestamp': datetime.now().isoformat()
}
)
4. Provide User Feedback
Give users helpful, actionable error messages:
ERROR_MESSAGES = {
400: "Please check your input and try again",
401: "Your session has expired. Please login again",
403: "You don't have permission to perform this action",
404: "The requested resource was not found",
429: "Too many requests. Please wait a moment and try again",
500: "Server error. Please try again later"
}
def get_user_friendly_message(status_code):
return ERROR_MESSAGES.get(status_code, "An unexpected error occurred")
5. Handle Token Expiration Gracefully
Automatically refresh tokens when they expire:
def ensure_valid_token(token_manager):
if token_manager.is_token_expired():
success = token_manager.refresh_access_token()
if not success:
# Redirect to login
raise AuthenticationError("Session expired. Please login again")
return token_manager.get_access_token()
def api_request_with_token_refresh(token_manager, endpoint, method='GET', **kwargs):
try:
headers = token_manager.get_auth_headers()
if method == 'GET':
response = requests.get(endpoint, headers=headers, **kwargs)
elif method == 'POST':
response = requests.post(endpoint, headers=headers, **kwargs)
if response.status_code == 401:
# Token might have been revoked, try refresh
token_manager.refresh_access_token()
headers = token_manager.get_auth_headers()
# Retry once
if method == 'GET':
response = requests.get(endpoint, headers=headers, **kwargs)
elif method == 'POST':
response = requests.post(endpoint, headers=headers, **kwargs)
return response
except Exception as e:
logger.error(f"API request failed: {e}")
raise
6. Validate Input Before Sending
Validate data before making API requests to catch errors early:
from decimal import Decimal
import phonenumbers
def validate_payout_request(payto, amount, ref_no):
"""Validate payout request data"""
errors = []
# Validate phone number
try:
parsed = phonenumbers.parse(payto, "KE")
if not phonenumbers.is_valid_number(parsed):
errors.append("Invalid phone number format")
except:
errors.append("Phone number parsing failed")
# Validate amount
try:
decimal_amount = Decimal(str(amount))
if decimal_amount < Decimal('1.00') or decimal_amount > Decimal('999999.99'):
errors.append("Amount must be between 1.00 and 999999.99")
if decimal_amount.as_tuple().exponent < -2:
errors.append("Amount must have maximum 2 decimal places")
except:
errors.append("Invalid amount format")
# Validate reference number
if not ref_no or len(ref_no) > 50:
errors.append("Reference number must be 1-50 characters")
return errors
# Usage
errors = validate_payout_request("+254712345678", "1000.00", "PAYOUT-001")
if errors:
for error in errors:
print(f"Validation error: {error}")
else:
# Proceed with API call
response = initiate_payout(...)
Rate Limiting
Understanding Rate Limits
The JPay Africa API implements rate limiting to ensure fair usage:
- Standard Rate Limit: 60 requests per minute (1 request per second)
- Burst Limit: 100 requests per 10 seconds
- Headers Returned:
X-RateLimit-Limit: Your rate limit (60)X-RateLimit-Remaining: Requests remainingX-RateLimit-Reset: Unix timestamp when limit resets
Handling Rate Limits
import time
from datetime import datetime
def api_call_with_rate_limit_handling(endpoint, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(endpoint, headers=headers)
if response.status_code == 429:
# Get reset time from header
reset_time = int(response.headers.get('X-RateLimit-Reset', 0))
wait_seconds = max(0, reset_time - int(datetime.now().timestamp()))
if attempt < max_retries - 1:
print(f"Rate limited. Waiting {wait_seconds} seconds...")
time.sleep(wait_seconds + 1) # Add 1 second buffer
continue
return response
return None
# Monitor rate limit consumption
def monitor_rate_limit(response):
remaining = response.headers.get('X-RateLimit-Remaining')
limit = response.headers.get('X-RateLimit-Limit')
if remaining:
print(f"Requests remaining: {remaining}/{limit}")
# Warn if approaching limit
if int(remaining) < 10:
print("Warning: Approaching rate limit")
Network Error Handling
Handle Connection Failures
import socket
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
def create_session_with_retries(max_retries=3):
"""Create requests session with automatic retry logic"""
session = requests.Session()
retry_strategy = Retry(
total=max_retries,
backoff_factor=1, # Wait 1, 2, 4 seconds between retries
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS", "POST"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
# Usage
session = create_session_with_retries()
response = session.post(url, json=payload, timeout=10)
Troubleshooting Guide
| Symptom | Possible Cause | Solution |
|---|---|---|
| 401 Unauthorized | Token expired | Refresh token before request |
| 403 Forbidden | Merchant not approved | Complete merchant verification |
| 400 Bad Request | Invalid phone format | Use E.164 format (e.g., +254712345678) |
| 400 Bad Request | Invalid amount format | Use decimal with 2 places (e.g., "1000.00") |
| 429 Too Many Requests | Rate limit exceeded | Wait and retry with exponential backoff |
| 500 Internal Server Error | Server error | Retry request, contact support if persists |
| Connection timeout | Network issue | Check internet connection, retry |
| SSL Certificate Error | Certificate issue | Update root certificates |
Related Resources
- Error Codes Reference - Comprehensive error code reference
- Best Practices - Integration best practices
- Authentication Overview - Authentication methods