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 /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/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​

import phonenumbers

def format_phone_number(phone):
parsed = phonenumbers.parse(phone, "KE")
if phonenumbers.is_valid_number(parsed):
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
return None

Idempotency​

Use unique reference numbers:

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

Wallet Balance Check​

Check balance before processing:

def check_payout_wallet(access_token):
response = requests.get(
'https://sandbox.api.jpay.africa/api/v1/wallets/balance',
headers={'Authorization': f'Bearer {access_token}'}
)

if response.status_code == 200:
return response.json()['payout_wallet_balance']
return None
tip

For detailed information, 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​