Payments API Overview
The JPay Africa Payments API allows you to process collections and payouts securely and efficiently.
API Endpoints
Collections
Process incoming payments from customers:
- Initiate Collection:
POST /collections/checkouts/initiate - List Collections:
GET /collections/checkouts - Get Collection Details:
GET /collections/checkouts/{checkout_id}
Payouts
Send payments to beneficiaries:
- Initiate Payout:
POST /payouts/initiate - List Payouts:
GET /payouts - Get Payout Details:
GET /payouts/{payout_id}
Key Concepts
Collection (Checkout)
A collection represents an incoming payment request. Customers are charged and funds are credited to your collection wallet.
Status: pending | processing | completed | failed
Payout
A payout represents an outgoing payment. Funds are debited from your payout wallet and sent to beneficiaries.
Status: pending | processing | completed | failed
Wallet Types
- Collection Wallet: Receives incoming payments
- Payout Wallet: Sends outgoing payments
Requirements
To use the Payments API:
- ✅ Merchant profile must be APPROVED
- ✅ App must have the required product enabled
- ✅ Valid access token with authentication
- ✅ Correct phone number format (E.164)
Transaction Flow
Collections Flow
1. Initiate Collection
↓
2. Customer receives payment prompt
↓
3. Payment processed
↓
4. Funds credited to collection wallet
↓
5. Webhook notification sent
Payouts Flow
1. Initiate Payout
↓
2. Funds debited from payout wallet
↓
3. Payment processed
↓
4. Beneficiary receives funds
↓
5. Webhook notification sent
Common Parameters
Phone Numbers
All phone numbers must be in E.164 format:
- Format:
+<country_code><phone_number> - Example:
+254712345678 - Kenya Code:
+254
Amounts
All amounts are in KES (Kenyan Shilling):
- Format: Decimal number with 2 decimal places
- Example:
1000.00 - Min:
1.00 - Max:
999999.99
Reference Numbers
Reference numbers are unique identifiers for tracking:
- Format: Alphanumeric string (max 50 characters)
- Example:
ORDER-2024-001 - Purpose: Link transactions to your internal orders
Error Handling
All API responses follow standard HTTP status codes:
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Proceed normally |
| 400 | Bad Request | Check request parameters |
| 401 | Unauthorized | Verify authentication |
| 403 | Forbidden | Check permissions/status |
| 404 | Not Found | Verify resource ID |
| 429 | Too Many Requests | Implement backoff |
| 500 | Server Error | Retry with backoff |
For detailed error information, see Error Handling.
Rate Limits
- Requests per minute: 60
- Burst limit: 100
- Daily limit: No limit (per merchant)
Pagination
List endpoints support pagination:
| Parameter | Type | Default | Max |
|---|---|---|---|
page | integer | 1 | N/A |
page_size | integer | 20 | 100 |
Example
GET /collections/checkouts?page=2&page_size=50
Filtering
List endpoints support filtering:
| Parameter | Type | Description |
|---|---|---|
status | integer | Filter by status |
date_from | string | Start date (YYYY-MM-DD) |
date_to | string | End date (YYYY-MM-DD) |
Example
GET /collections/checkouts?status=1&date_from=2024-01-01&date_to=2024-12-31
Webhooks
Get real-time notifications for transaction events:
- Endpoint: Your callback URL
- Method: POST
- Payload: JSON with transaction details
Webhook Events
collection.created- Collection initiatedcollection.completed- Collection successfulcollection.failed- Collection failedpayout.created- Payout initiatedpayout.completed- Payout successfulpayout.failed- Payout failed
Webhooks are sent to your callback URL when transactions are completed. See endpoint documentation for webhook payload examples.
Webhook Security & Signature Verification
To prevent spoofing, replay attacks, and ensure webhook requests originate from JPay, you must verify the webhook signature on every callback request.
JPay signs each webhook payload with your merchant App Secret (or callback signing secret) and includes the signature and timestamp in the request headers:
| Header | Description |
|---|---|
X-JPay-Signature | The hex-encoded HMAC-SHA256 signature of the timestamp and payload. |
X-JPay-Timestamp | The Unix timestamp (in seconds) of when the request was dispatched. |
Verification Steps
- Extract Headers: Retrieve the values of the
X-JPay-SignatureandX-JPay-Timestampheaders. - Prevent Replay Attacks: Compute the age of the request (
current_time - timestamp). We recommend rejecting any requests older than 5 minutes (300 seconds). - Build the Signature Material: Concatenate the string value of the timestamp, a literal period
., and the raw, unparsed request body bytes:signature_material = timestamp + "." + raw_body_bytes - Compute the Expected Signature: Calculate the hex HMAC-SHA256 hash of the signature material using your merchant App Secret as the key.
- Constant-Time Comparison: Use a constant-time comparison function (to avoid timing attacks) to verify if your calculated signature matches the
X-JPay-Signatureheader value.
Verification Examples
- Node.js (Express)
- Python (Flask / Django)
- PHP
- Go
const crypto = require('crypto');
function verifyWebhook(req, res, next) {
const signature = req.headers['x-jpay-signature'];
const timestamp = req.headers['x-jpay-timestamp'];
// 1. Ensure headers exist
if (!signature || !timestamp) {
return res.status(401).send('Missing security headers');
}
// 2. Prevent replay attacks (5 minute threshold)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
return res.status(401).send('Request timestamp too old');
}
// 3. Reconstruct signature material (req.rawBody must be a Buffer containing the raw body)
const appSecret = process.env.JPAY_APP_SECRET;
const signatureMaterial = Buffer.concat([
Buffer.from(`${timestamp}.`),
req.rawBody // Ensure your Express app has a middleware to preserve req.rawBody
]);
// 4. Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', appSecret)
.update(signatureMaterial)
.digest('hex');
// 5. Constant-time comparison
const isMatch = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
if (!isMatch) {
return res.status(401).send('Invalid signature');
}
next();
}
import hmac
import hashlib
import time
def verify_jpay_webhook(raw_body_bytes: bytes, headers: dict, app_secret: str) -> bool:
signature = headers.get('X-JPay-Signature')
timestamp = headers.get('X-JPay-Timestamp')
# 1. Ensure headers exist
if not signature or not timestamp:
return False
# 2. Prevent replay attacks
try:
ts_int = int(timestamp)
except ValueError:
return False
if abs(time.time() - ts_int) > 300:
return False # Request too old
# 3. Build signature material
signed_material = f"{timestamp}.".encode('utf-8') + raw_body_bytes
# 4. Compute expected signature
expected = hmac.new(
app_secret.encode('utf-8'),
signed_material,
hashlib.sha256
).hexdigest()
# 5. Constant-time comparison
return hmac.compare_digest(expected, signature)
<?php
function verifyJPayWebhook($rawBody, $headers, $appSecret) {
$signature = $headers['X-JPay-Signature'] ?? null;
$timestamp = $headers['X-JPay-Timestamp'] ?? null;
// 1. Ensure headers exist
if (!$signature || !$timestamp) {
return false;
}
// 2. Prevent replay attacks
if (abs(time() - (int)$timestamp) > 300) {
return false;
}
// 3. Build signature material
$signatureMaterial = $timestamp . '.' . $rawBody;
// 4. Compute expected signature
$expectedSignature = hash_hmac('sha256', $signatureMaterial, $appSecret);
// 5. Constant-time comparison
return hash_equals($expectedSignature, $signature);
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"strconv"
"time"
)
func VerifyJPayWebhook(rawBody []byte, headers map[string]string, appSecret string) bool {
signature := headers["X-JPay-Signature"]
timestampStr := headers["X-JPay-Timestamp"]
// 1. Ensure headers exist
if signature == "" || timestampStr == "" {
return false
}
// 2. Prevent replay attacks
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return false
}
if math.Abs(float64(time.Now().Unix()-timestamp)) > 300 {
return false
}
// 3. Build signature material
signatureMaterial := []byte(fmt.Sprintf("%s.", timestampStr))
signatureMaterial = append(signatureMaterial, rawBody...)
// 4. Compute expected signature
mac := hmac.New(sha256.New, []byte(appSecret))
mac.Write(signatureMaterial)
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// 5. Constant-time comparison
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
Next Steps
- Initiate Collection - Accept payments
- Initiate Payout - Send payments
- List Transactions - Retrieve history