Skip to main content

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

CodeMeaningDescription
200OKRequest successful, response contains data
201CreatedResource successfully created
204No ContentRequest successful, no content to return

4xx Client Errors

CodeMeaningDescription
400Bad RequestInvalid request parameters or malformed data
401UnauthorizedAuthentication failed or token invalid/expired
403ForbiddenAuthenticated but not authorized for this resource
404Not FoundRequested resource does not exist
429Too Many RequestsRate limit exceeded

5xx Server Errors

CodeMeaningDescription
500Internal Server ErrorUnexpected server error
502Bad GatewayTemporary server connectivity issue
503Service UnavailableServer 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 remaining
    • X-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

SymptomPossible CauseSolution
401 UnauthorizedToken expiredRefresh token before request
403 ForbiddenMerchant not approvedComplete merchant verification
400 Bad RequestInvalid phone formatUse E.164 format (e.g., +254712345678)
400 Bad RequestInvalid amount formatUse decimal with 2 places (e.g., "1000.00")
429 Too Many RequestsRate limit exceededWait and retry with exponential backoff
500 Internal Server ErrorServer errorRetry request, contact support if persists
Connection timeoutNetwork issueCheck internet connection, retry
SSL Certificate ErrorCertificate issueUpdate root certificates