Initiate Collection (Checkout)
The Initiate Collection endpoint allows you to request payment from a customer. Use this when you want to collect money from users.
Collections currently only support mobile money channels (M-Pesa, Airtel Money, T-Kash). Bank transfers are not supported for collections.
Endpoint
POST /payments/collections/checkouts/initiate
Authentication
Required: Yes (Bearer Token)
Authorization: Bearer YOUR_ACCESS_TOKEN
Request
Request Body
{
"payfrom": "+254712345678",
"amount": "1000.00",
"ref_no": "ORDER-2024-001",
"callback_url": "https://yourdomain.com/webhook",
"category": "ecommerce",
"narration": "Payment for Order #123",
"channel": 63902,
"app_code": 12345
}
Field Descriptions
| Field | Type | Required | Description |
|---|---|---|---|
payfrom | string | Yes | Customer phone number (E.164 format) |
amount | string | Yes | Payment amount in KES (decimal with 2 places) |
ref_no | string | Yes | Your unique reference number (max 50 chars) |
account_number | string | No | Your account number for tracking. If not provided, defaults to the platform product name. |
callback_url | string | Yes | URL for transaction notifications |
category | string | No | Transaction category: ecommerce, gaming, adult_content, forex, or other (default) |
narration | string | No | Payment description/narration sent to payment gateway (max 200 chars, default: "Request Payment") |
channel | integer | No | Payment channel code (e.g., 63902 for M-Pesa). Not required when payfrom is a phone number - the channel is automatically inferred from the phone carrier (Safaricom → M-Pesa, Airtel → Airtel Money, Telkom → T-Kash). See List Channels for available channels. Note: Collections only support mobile money channels. |
app_code | integer | Yes | App code for multi-app scenarios |
Response
Success Response (200 OK)
{
"payfrom": "+254712345678",
"amount": "1000.00",
"ref_no": "ORDER-2024-001",
"account_number": "1001",
"callback_url": "https://yourdomain.com/webhook",
"status": 0,
"category": "ecommerce",
"channel": 63902,
"channel_name": "M-Pesa",
"channel_type": "Mobile Money",
"created_at": "12/04/2024 14:30:45"
}
Response Fields
| Field | Type | Description |
|---|---|---|
payfrom | string | Customer phone number |
amount | string | Payment amount (KES) |
ref_no | string | Your reference number |
account_number | string | Account number used |
callback_url | string | Webhook notification URL |
status | integer | Transaction status code (0=Pending, 1=Processed, 2=Failed, 3=Completed) |
category | string | Transaction category |
channel | integer | Payment channel code |
channel_name | string | Payment channel name (e.g., "M-Pesa", "Airtel Money") |
channel_type | string | Channel type ("Mobile Money" or "Bank") |
created_at | string | Timestamp (DD/MM/YYYY HH:MM:SS) |
Status Codes
| Code | Status | Description |
|---|---|---|
| 200 | Success | Collection initiated successfully |
| 400 | Bad Request | Invalid parameters |
| 401 | Unauthorized | Invalid or expired token |
| 403 | Forbidden | Merchant not approved or app inactive |
| 404 | Not Found | App not found |
| 500 | Server Error | Internal server error |
Error Responses
400 Bad Request - Invalid Phone Number
{
"detail": "Invalid phone number"
}
400 Bad Request - Invalid Amount
{
"detail": "Invalid amount"
}
400 Bad Request - Invalid Channel
{
"detail": "Invalid channel code: 99999"
}
400 Bad Request - Invalid Channel Type
{
"detail": "Collections only support mobile money channels. NCBA Bank is a Bank."
}
401 Unauthorized
{
"detail": "Invalid or expired token"
}
403 Forbidden - Merchant Not Approved
{
"detail": "Your merchant profile is not approved. Please ensure your profile has been verified before initiating checkouts."
}
403 Forbidden - App Inactive
{
"detail": "App is not active"
}
403 Forbidden - Product Not Enabled
{
"detail": "App does not have collections product enabled"
}
403 Forbidden - IP Not Allowed
{
"detail": "Access denied: IP address not allowed"
}
This error occurs when your app has IP restrictions configured and the request is coming from an IP address that is not in the allowed list. To fix this:
- Add your server's IP address to the allowed IPs list in your app settings
- Or remove all IP restrictions if you want to allow requests from any IP address
404 Not Found
{
"detail": "App not found"
}
Examples
- cURL
- Python
- JavaScript
curl -X POST https://sandbox.api.jpay.africa/api/v1/payments/collections/checkouts/initiate \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Content-Type: application/json" \
-d '{
"payfrom": "+254712345678",
"amount": "1000.00",
"ref_no": "ORDER-2024-001",
"callback_url": "https://yourdomain.com/webhook",
"category": "ecommerce",
"narration": "Payment for Order #123",
"channel": 63902
}'
import requests
def initiate_collection(access_token, payfrom, amount, ref_no):
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
payload = {
'payfrom': payfrom,
'amount': str(amount),
'ref_no': ref_no,
'callback_url': 'https://yourdomain.com/webhook',
'category': 'ecommerce',
'narration': 'Payment for Order #123',
'channel': 63902 # Optional: M-Pesa channel code
}
response = requests.post(
'https://sandbox.api.jpay.africa/api/v1/payments/collections/checkouts/initiate',
headers=headers,
json=payload
)
if response.status_code == 200:
return response.json()
else:
print(f"Error: {response.status_code}")
print(response.json())
return None
# Usage
collection = initiate_collection(
access_token='your_token',
payfrom='+254712345678',
amount=1000.00,
ref_no='ORDER-2024-001'
)
async function initiateCollection(accessToken, payfrom, amount, refNo) {
const response = await fetch(
'https://sandbox.api.jpay.africa/api/v1/payments/collections/checkouts/initiate',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
payfrom,
amount: amount.toString(),
ref_no: refNo,
callback_url: 'https://yourdomain.com/webhook',
category: 'ecommerce',
narration: 'Payment for Order #123',
channel: 63902 // Optional: M-Pesa channel code
})
}
);
const data = await response.json();
if (response.ok) {
console.log('Collection initiated:', data);
return data;
} else {
console.error('Error:', data);
throw new Error(data.detail);
}
}
// Usage
try {
const collection = await initiateCollection(
'your_token',
'+254712345678',
1000.00,
'ORDER-2024-001'
);
} catch (error) {
console.error('Failed to initiate collection:', error);
}
Collection Categories
Use appropriate categories for collection organization and compliance:
| Category | Description |
|---|---|
ecommerce | E-commerce and online shopping transactions |
gaming | Gaming and entertainment transactions |
adult_content | Adult content related transactions |
forex | Forex and trading transactions |
other | Other transaction types (default) |
Category Usage: Categories help with transaction organization, reporting, and compliance. Choose the category that best matches your business type.
Important Notes
Before initiating collections, ensure:
- ✅ Your merchant account is APPROVED (profile_status = approved)
- ✅ Your app has collections product enabled
- ✅ Your app is ACTIVE
- ✅ Phone number is in E.164 format (e.g., +254712345678)
- ✅ Amount has 2 decimal places (e.g., 1000.00)
- ✅ Your server's IP address is whitelisted in the app's allowed IPs (if IP restrictions are configured)
Best Practices
Phone Number Validation
Always validate and format phone numbers before sending:
import phonenumbers
def format_phone_number(phone):
try:
parsed = phonenumbers.parse(phone, "KE")
if phonenumbers.is_valid_number(parsed):
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
except phonenumbers.NumberParseException:
pass
return None
Error Handling
Implement comprehensive error handling:
def create_collection_with_retry(client, max_retries=3):
for attempt in range(max_retries):
try:
response = requests.post(...)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
# Token expired, refresh and retry
client.refresh_token()
continue
elif response.status_code == 403:
# Merchant not approved, stop
print("Merchant account not approved")
break
elif response.status_code == 429:
# Rate limited, wait and retry
time.sleep(2 ** attempt)
continue
else:
print(f"Unexpected error: {response.status_code}")
break
except requests.RequestException as e:
print(f"Request failed: {e}")
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
Amount Validation
Validate amounts before sending:
from decimal import Decimal
def validate_amount(amount):
try:
decimal_amount = Decimal(str(amount))
# Check range
if decimal_amount < Decimal('1.00') or decimal_amount > Decimal('999999.99'):
return False, "Amount must be between 1.00 and 999999.99"
# Check decimal places
if decimal_amount.as_tuple().exponent < -2:
return False, "Amount must have maximum 2 decimal places"
return True, decimal_amount
except:
return False, "Invalid amount format"
Webhook Notifications
When a collection is processed, JPay will POST to your callback_url with a standardized payload:
{
"notification": {
"type": "collection",
"event": "transaction.completed"
},
"data": {
"result_code": 0,
"result_description": "Transaction processed successfully",
"amount": "1000.00",
"ref_no": "ORDER-2024-001",
"beneficiary": {
"account": "+254712345678",
"kyc": null
},
"transaction_date": "2024-04-12T14:35:00.000Z",
"external_ref": "SPEJ12345",
"trans_id": "QGX1234567"
}
}
Webhook Payload Structure
Notification Object
| Field | Type | Description |
|---|---|---|
type | string | Transaction type: collection or payout |
event | string | Event type: transaction.created, transaction.failed, or transaction.completed |
Data Object
| Field | Type | Description |
|---|---|---|
result_code | integer | Result code: 0 for success, any other value indicates failure |
result_description | string | Description of the transaction result or failure reason |
amount | string | Transaction amount in KES |
ref_no | string | Your reference number provided during initiation |
beneficiary | object | Beneficiary information |
beneficiary.account | string | Customer phone number (payfrom field) in E.164 format |
beneficiary.kyc | string or null | Full names of the payer. Currently null for collections |
transaction_date | string | Transaction timestamp in ISO 8601 format (UTC) |
external_ref | string | External transaction reference from the payment gateway (may be empty) |
trans_id | string | Payment gateway transaction code (may be empty) |
Event Types
transaction.created- Transaction has been initiatedtransaction.completed- Transaction completed successfully (result_code = 0)transaction.failed- Transaction failed (result_code ≠ 0)
Result Code: A result_code of 0 indicates successful completion. Any other value indicates a failure, with details provided in result_description.
Transaction Date: The transaction_date field uses the completion time from the payment gateway when available, otherwise falls back to the creation time.
Next Steps
- List Collections - Retrieve collection history
- Error Handling - Handle API errors
- Best Practices - Integration best practices