Best Practices
Essential recommendations for secure and efficient JPay Africa API integration.
🎯 Quick Essentials
- ✅ Store credentials in environment variables - Never hardcode API keys
- ✅ Use unique reference numbers - Ensures idempotency and prevents duplicates
- ✅ Validate phone numbers - Always use E.164 format (+254...)
- ✅ Implement retry logic - Handle transient network failures gracefully
- ✅ Enable IP whitelisting - Restrict API access in production
- ✅ Handle webhooks - Don't poll for transaction status
- ✅ Use HTTPS only - Never use HTTP for API requests
- ✅ Log errors, not secrets - Sanitize tokens from logs
Authentication Best Practices
1. Secure Token Storage
Server-Side (Recommended):
import os
from dotenv import load_dotenv
load_dotenv()
APP_KEY = os.getenv('JPAY_APP_KEY')
APP_SECRET = os.getenv('JPAY_APP_SECRET')
Never do this:
# ❌ DO NOT hardcode credentials
APP_KEY = 'your_actual_key' # NEVER DO THIS
2. Token Refresh Strategy
Refresh tokens before they expire (not after):
import jwt
from datetime import datetime, timedelta
def should_refresh_token(access_token):
"""Check if token expires in less than 5 minutes"""
decoded = jwt.decode(access_token, options={"verify_signature": False})
expiry = datetime.fromtimestamp(decoded['exp'])
return expiry - datetime.now() < timedelta(minutes=5)
3. IP Whitelisting (Production Only)
Configure in your app settings:
App Settings → Allowed IPs → Add IP addresses
Example:
203.0.113.42 # Production server 1
203.0.113.43 # Production server 2
198.51.100.0/24 # Office network
Benefits: Prevents unauthorized access even if credentials are compromised.
tip
Find your server IP: curl https://api.ipify.org
Data Validation
1. Phone Number Validation
Always validate to E.164 format:
import phonenumbers
def validate_phone(phone, country="KE"):
try:
parsed = phonenumbers.parse(phone, country)
if phonenumbers.is_valid_number(parsed):
return phonenumbers.format_number(
parsed, phonenumbers.PhoneNumberFormat.E164
)
except:
pass
return None
# Usage
phone = validate_phone("0712345678") # Returns: +254712345678
2. Amount Validation
from decimal import Decimal
def validate_amount(amount):
decimal_amount = Decimal(str(amount))
if decimal_amount < Decimal('1.00') or decimal_amount > Decimal('999999.99'):
return False, "Amount must be between 1.00 and 999999.99"
if decimal_amount.as_tuple().exponent < -2:
return False, "Maximum 2 decimal places"
return True, f"{decimal_amount:.2f}"
3. Unique Reference Numbers
import uuid
from datetime import datetime
def generate_ref_no(prefix="TXN"):
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
unique_id = str(uuid.uuid4())[:6].upper()
return f"{prefix}-{timestamp}-{unique_id}"
# Example: TXN-20240430143045-ABC123
Error Handling & Retry Logic
Implement Exponential Backoff
import time
import requests
def api_request_with_retry(url, headers, data, max_retries=3):
for attempt in range(max_retries):
try:
response = requests.post(url, headers=headers, json=data, timeout=10)
# Don't retry client errors (except rate limits)
if 400 <= response.status_code < 500 and response.status_code != 429:
return response
# Retry on 429 (rate limit) or 500+ (server errors)
if response.status_code == 429 or response.status_code >= 500:
if attempt < max_retries - 1:
wait = 2 ** attempt # 1s, 2s, 4s
time.sleep(wait)
continue
return response
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
else:
raise
return None
Webhook Handling
Validate and Process Webhooks
from flask import request
@app.route('/webhook', methods=['POST'])
def handle_webhook():
data = request.json
# Extract key information
event_type = data['notification']['type'] # 'collection' or 'payout'
result_code = data['data']['result_code'] # 0 = success
ref_no = data['data']['ref_no']
# Process based on result
if result_code == 0:
# Transaction successful
update_order_status(ref_no, 'PAID')
fulfill_order(ref_no)
else:
# Transaction failed
result_desc = data['data']['result_description']
log_failed_transaction(ref_no, result_desc)
return {'status': 'ok'}, 200
info
Important: Always respond with 200 OK to acknowledge webhook receipt, even if processing fails.
Security Best Practices
1. Use HTTPS Only
# ✅ Always use HTTPS
API_URL = "https://sandbox.api.jpay.africa/api/v1"
# ❌ Never use HTTP
API_URL = "http://api.jpay.africa/api/v1" # INSECURE
2. Sanitize Logs
import re
def sanitize_log(message):
# Remove Bearer tokens
message = re.sub(r'Bearer\s+[a-zA-Z0-9\-_.]+', 'Bearer [REDACTED]', message)
# Remove app_secret
message = re.sub(r'"app_secret"\s*:\s*"[^"]*"', '"app_secret": "[REDACTED]"', message)
return message
3. IP Whitelisting Configuration
Production Setup:
- Go to App Settings → Allowed IPs
- Add your production server IPs
- Test from whitelisted IP
- Remove test/development IPs
Benefits:
- ✅ Prevents unauthorized access even if credentials leak
- ✅ Required for PCI-DSS compliance
- ✅ Protects against token theft
Performance Optimization
1. Use Connection Pooling
import requests
from requests.adapters import HTTPAdapter
session = requests.Session()
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10)
session.mount('https://', adapter)
# Reuse session for multiple requests
response = session.post(url, headers=headers, json=data)
2. Rate Limiting
Respect the 60 requests/minute limit:
import time
class RateLimiter:
def __init__(self, max_per_minute=50): # Leave buffer
self.min_interval = 60 / max_per_minute
self.last_request = 0
def wait_if_needed(self):
elapsed = time.time() - self.last_request
if elapsed < self.min_interval:
time.sleep(self.min_interval - elapsed)
self.last_request = time.time()
limiter = RateLimiter()
for item in items:
limiter.wait_if_needed()
make_api_request(item)
Testing Best Practices
Use Sandbox Environment
# Development
SANDBOX_URL = "https://sandbox.api.jpay.africa/api/v1"
SANDBOX_APP_KEY = os.getenv('JPAY_SANDBOX_KEY')
# Production
PROD_URL = "https://api.jpay.africa/api/v1"
PROD_APP_KEY = os.getenv('JPAY_PROD_KEY')
# Switch based on environment
API_URL = SANDBOX_URL if os.getenv('ENV') == 'dev' else PROD_URL
Test Phone Numbers
- Collection:
+254712345670 - Payout:
+254712345671
Quick Reference Checklist
Before Going Live:
- Environment variables configured (no hardcoded credentials)
- Phone number validation implemented
- Amount validation with 2 decimal places
- Unique reference numbers for each transaction
- Retry logic with exponential backoff
- Webhook endpoint implemented and tested
- IP whitelisting configured
- HTTPS enforced
- Error logging (with sanitization)
- Rate limiting implemented
- Tested in sandbox environment