Skip to main content

Best Practices

Essential recommendations for secure and efficient JPay Africa API integration.

🎯 Quick Essentials

  1. Store credentials in environment variables - Never hardcode API keys
  2. Use unique reference numbers - Ensures idempotency and prevents duplicates
  3. Validate phone numbers - Always use E.164 format (+254...)
  4. Implement retry logic - Handle transient network failures gracefully
  5. Enable IP whitelisting - Restrict API access in production
  6. Handle webhooks - Don't poll for transaction status
  7. Use HTTPS only - Never use HTTP for API requests
  8. 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:

  1. Go to App Settings → Allowed IPs
  2. Add your production server IPs
  3. Test from whitelisted IP
  4. 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