Skip to main content

Initiate Payout

The Initiate Payout endpoint allows you to send money to a customer. Use this to process refunds, disbursements, commission payments, or any other payout scenarios.

Endpoint

POST /payments/payouts/initiate

Authentication

Required: Yes (Bearer Token)

Authorization: Bearer YOUR_ACCESS_TOKEN

Request

Request Body

{
"payto": "+254712345678",
"amount": "1000.00",
"ref_no": "PAYOUT-2024-001",
"callback_url": "https://yourdomain.com/webhook",
"category": "other",
"narration": "Commission Payment #456",
"channel": 63902,
"app_code": 12345
}

Field Descriptions

FieldTypeRequiredDescription
paytostringYesRecipient phone number (E.164 format, e.g., +254712345678) or bank account number
amountstringYesPayout amount in KES (decimal with 2 places)
ref_nostringYesYour unique reference number (max 50 chars)
callback_urlstringYesURL for transaction notifications
categorystringNoTransaction category: ecommerce, gaming, adult_content, forex, or other (default)
narrationstringNoDisbursement description/narration sent to payment gateway (max 200 chars, default: "Disbursement")
channelintegerNoPayment channel code (e.g., 63902 for M-Pesa). Not required when payto is a phone number - the channel is automatically inferred from the phone carrier (Safaricom → M-Pesa, Airtel → Airtel Money, Telkom → T-Kash). Required for bank transfers - specify the bank channel code. See List Channels for available channels.
app_codeintegerYesApp code for multi-app scenarios

Response

Success Response (200 OK)

{
"id": "PAYOUT-2024-001-ABC123",
"payto": "+254712345678",
"amount": "1000.00",
"ref_no": "PAYOUT-2024-001",
"status": "pending",
"category": "other",
"channel": 63902,
"channel_name": "M-Pesa",
"channel_type": "Mobile Money",
"created_at": "12/04/2024 14:30:45"
}

Response Fields

FieldTypeDescription
idstringUnique payout transaction identifier
paytostringRecipient phone number
amountstringPayout amount (KES)
ref_nostringYour reference number
statusstringTransaction status
categorystringTransaction category
channelintegerPayment channel code
channel_namestringPayment channel name (e.g., "M-Pesa", "Airtel Money", "NCBA Bank")
channel_typestringChannel type ("Mobile Money" or "Bank")
created_atstringTimestamp (DD/MM/YYYY HH:MM:SS)

Status Codes

CodeStatusDescription
200SuccessPayout initiated successfully
400Bad RequestInvalid parameters
401UnauthorizedInvalid or expired token
403ForbiddenMerchant not approved or app inactive
404Not FoundApp not found
500Server ErrorInternal 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 - Insufficient Balance

{
"detail": "Insufficient balance in payout wallet"
}

400 Bad Request - Invalid Channel

{
"detail": "Invalid channel code: 99999"
}

400 Bad Request - Channel Required for Bank Transfer

{
"detail": "Channel is required for bank account payouts"
}

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 payouts."
}

403 Forbidden - App Inactive

{
"detail": "App is not active"
}

403 Forbidden - Product Not Enabled

{
"detail": "App does not have payouts product enabled"
}

403 Forbidden - IP Not Allowed

{
"detail": "Access denied: IP address not allowed"
}
info

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 -X POST https://sandbox.api.jpay.africa/api/v1/payments/payouts/initiate \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Content-Type: application/json" \
-d '{
"payto": "+254712345678",
"amount": "1000.00",
"ref_no": "PAYOUT-2024-001",
"callback_url": "https://yourdomain.com/webhook",
"category": "other",
"narration": "Commission Payment #456",
"channel": 63902
}'

Payout Categories

Use appropriate categories for payout organization and compliance:

CategoryDescription
ecommerceE-commerce and online shopping payouts
gamingGaming and entertainment payouts
adult_contentAdult content related payouts
forexForex and trading payouts
otherOther payout types (default)
info

Category Usage: Categories help with transaction organization, reporting, and compliance. Choose the category that best matches your business type.

Payout Lifecycle

Initiated

[pending] ← Processing (2-4 hours)

[completed] ← Successfully delivered

Recipient receives funds

or

[failed] ← Delivery failed

Automatic refund to sender's wallet

Payout Wallet

Your merchant account has a Payout Wallet that holds funds for sending payouts:

  • Funding: Payouts are debited from your collection wallet or funded through bank deposit
  • Balance: Check your balance before initiating payouts to avoid failures
  • Settlement: Funds settle within 2-4 hours for mobile, 24-48 hours for bank transfers

Important Notes

danger

Before initiating payouts, ensure:

  1. ✅ Your merchant account is APPROVED (profile_status = approved)
  2. ✅ Your app has payouts product enabled
  3. ✅ Your app is ACTIVE
  4. ✅ Phone number is in E.164 format (e.g., +254712345678)
  5. ✅ Amount has 2 decimal places (e.g., 1000.00)
  6. ✅ Your payout wallet has sufficient balance
  7. ✅ 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

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"

Idempotency

Use unique reference numbers to ensure idempotency:

import uuid
from datetime import datetime

def generate_unique_ref_no(prefix="PAYOUT"):
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
unique_id = str(uuid.uuid4())[:8].upper()
return f"{prefix}-{timestamp}-{unique_id}"

# Usage
ref_no = generate_unique_ref_no()
# Result: PAYOUT-20240412143045-ABC12345

Error Handling with Retry

Implement comprehensive error handling:

def create_payout_with_retry(token_manager, payto, amount, ref_no, max_retries=3):
for attempt in range(max_retries):
try:
headers = token_manager.get_auth_headers()
response = requests.post(
'https://sandbox.api.jpay.africa/api/v1/payments/payouts/initiate',
headers=headers,
json={
'payto': payto,
'amount': str(amount),
'ref_no': ref_no
}
)

if response.status_code == 200:
return response.json()

elif response.status_code == 401:
# Token expired, refresh and retry
token_manager.refresh_access_token()
continue

elif response.status_code == 403:
# Merchant not approved or insufficient balance
print("Payout cannot be processed")
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)

return None

Wallet Balance Check

Check your payout wallet balance before processing:

def check_payout_wallet(access_token):
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}

response = requests.get(
'https://sandbox.api.jpay.africa/api/v1/wallets/balance',
headers=headers
)

if response.status_code == 200:
data = response.json()
return {
'payout_wallet_balance': data['payout_wallet_balance'],
'collection_wallet_balance': data['collection_wallet_balance'],
'total_balance': data['total_balance']
}
return None
tip

For detailed information on checking wallet balances, see Get Wallet Balance.

Webhook Notifications

When a payout is processed, JPay will POST to your callback_url with a standardized payload:

{
"notification": {
"type": "payout",
"event": "transaction.completed"
},
"data": {
"result_code": 0,
"result_description": "The service request is processed successfully",
"amount": "1000.00",
"ref_no": "PAYOUT-2024-001",
"beneficiary": {
"account": "+254712345678",
"kyc": null
},
"transaction_date": "2024-04-12T14:35:00.000Z",
"external_ref": "PR52345",
"trans_id": "QGX7654321"
}
}

Webhook Payload Structure

Notification Object

FieldTypeDescription
typestringTransaction type: collection or payout
eventstringEvent type: transaction.created, transaction.failed, or transaction.completed

Data Object

FieldTypeDescription
result_codeintegerResult code: 0 for success, any other value indicates failure
result_descriptionstringDescription of the transaction result or failure reason
amountstringTransaction amount in KES
ref_nostringYour reference number provided during initiation
beneficiaryobjectBeneficiary information
beneficiary.accountstringRecipient phone number (payto field) in E.164 format
beneficiary.kycstring or nullFull names of the recipient. Currently null for payouts
transaction_datestringTransaction timestamp in ISO 8601 format (UTC)
external_refstringExternal transaction reference from the payment gateway (may be empty)
trans_idstringPayment gateway transaction code (may be empty)

Event Types

  • transaction.created - Transaction has been initiated
  • transaction.completed - Transaction completed successfully (result_code = 0)
  • transaction.failed - Transaction failed (result_code ≠ 0)
info

Result Code: A result_code of 0 indicates successful completion. Any other value indicates a failure, with details provided in result_description.

info

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