Skip to contentSkip to Content

Payouts API

Send payments to till/paybill numbers and mobile money wallets across Africa.

Mobile Money Transfer

Initiate a payout to a mobile money wallet.

POST /business/payout/transfer

Mobile Money Request Body

FieldTypeRequiredDescription
senderCurrencystringYesSender’s currency code (USD, EUR, etc.)
receiverCurrencystringYesReceiver’s currency code (KES, TZS, UGX, ZAR)
amountnumberYesAmount to send in receiver’s currency
senderFirstNamestringYesSender’s first name
senderLastNamestringYesSender’s last name
senderDobstringYesSender’s date of birth (YYYY-MM-DD)
senderIdTypestringYesID type: passport, national_id, driving_license
senderIdNumberstringYesSender’s ID number
senderNationalitystringYesSender’s nationality (2-letter country code)
senderTelephoneNostringYesSender’s phone number with country code
receiverFirstNamestringYesReceiver’s first name
receiverLastNamestringYesReceiver’s last name
receiverPhonestringYesReceiver’s phone number with country code
relationshipstringYesRelationship to receiver (e.g., “Family”, “Business”)
payoutCountrystringYesDestination country code (KE, TZ, UG, ZA)
purposeOfTransferstringYesPurpose (see allowed values below)
sourceOfFundsstringYesSource of funds (see allowed values below)
businessReferencestringYesYour unique transaction reference
callbackUrlstringNoAbsolute callback URL for this payout’s final status
forceStatusstringTest onlyForce a TESTENV outcome (see Test Payout Outcomes)

Example Request - Kenya Mobile Money

curl -X POST https://api.test.wakapay.io/business/payout/transfer \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "senderCurrency": "USD", "receiverCurrency": "KES", "amount": 100.0, "senderFirstName": "Alice", "senderLastName": "Smith", "senderDob": "1985-03-15", "senderIdType": "passport", "senderIdNumber": "AB1234567", "senderNationality": "TZ", "senderTelephoneNo": "+255712345678", "receiverFirstName": "John", "receiverLastName": "Doe", "receiverPhone": "+254700000001", "relationship": "Family", "payoutCountry": "KE", "purposeOfTransfer": "family_support", "sourceOfFunds": "salary", "businessReference": "TEST-TRANSFER-KE-001", "callbackUrl": "https://partner.example.com/webhooks/wakapay/TEST-TRANSFER-KE-001" }'
const response = await fetch( "https://api.test.wakapay.io/business/payout/transfer", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ senderCurrency: "USD", receiverCurrency: "KES", amount: 100.0, senderFirstName: "Alice", senderLastName: "Smith", senderDob: "1985-03-15", senderIdType: "passport", senderIdNumber: "AB1234567", senderNationality: "TZ", senderTelephoneNo: "+255712345678", receiverFirstName: "John", receiverLastName: "Doe", receiverPhone: "+254700000001", relationship: "Family", payoutCountry: "KE", purposeOfTransfer: "family_support", sourceOfFunds: "salary", businessReference: "TEST-TRANSFER-KE-001", callbackUrl: "https://partner.example.com/webhooks/wakapay/TEST-TRANSFER-KE-001", }), }, ) const data = await response.json() console.log(data)

Mobile Money Response (201 Created)

{ "businessReference": "TEST-TRANSFER-KE-001", "receiverAmount": 100, "receiverCurrency": "KES", "senderAmount": 0.7751937984496124, "senderCurrency": "USD", "status": "termination_pending", "totalDebited": 0.7906976744186047, "wakapayReference": "0ad0a4a6-364b-11f1-8c14-0242ac120008" }

Response Fields

FieldTypeDescription
wakapayReferencestringWakapay transaction ID
businessReferencestringYour reference for tracking
statusstringTransaction status (termination_success, termination_pending, termination_failure)
senderAmountnumberAmount debited from sender wallet
senderCurrencystringSender’s currency
receiverAmountnumberAmount received by recipient
receiverCurrencystringReceiver’s currency
totalDebitednumberTotal amount debited (includes fees)

Per-request Callback URL

Use the optional callbackUrl field to receive the final status webhook at a URL selected for one payout. The value must be an absolute http:// or https:// URL. Use HTTPS in production.

Wakapay resolves the callback destination in this order:

  1. If the payout request includes callbackUrl, Wakapay uses it for that payout only.
  2. Otherwise, Wakapay uses the business account’s saved default callback URL.
  3. If neither URL is set, no webhook is sent. Poll GET /business/transactions/{businessReference} for the final status.

The webhook payload and signature do not change. Only the destination URL changes. Wakapay sends the callback when the payout reaches termination_success or termination_failure.

To use the account default, omit the field:

{ "businessReference": "PAY-2026-00043" }

Payment to Paybill / Till

Initiate a payment to a Paybill or Till number.

POST /business/payout/payment

Difference between Till and Paybill:

  • Till (Lipa Number) - No account reference needed. Payment goes directly to the till number.
  • Paybill - Requires an account reference (receiverAccount). Payment goes to the paybill number with a specific account reference for tracking.

Till/Paybill Request Body

FieldTypeRequiredDescription
typestringYesPayment type: paybill or till
senderCurrencystringYesSender’s currency code (USD, EUR, etc.)
receiverCurrencystringYesReceiver’s currency code (KES, TZS, UGX, ZAR)
amountnumberYesAmount to send in receiver’s currency
senderFirstNamestringYesSender’s first name
senderLastNamestringYesSender’s last name
senderDobstringYesSender’s date of birth (YYYY-MM-DD)
senderIdTypestringYesID type: passport, national_id, driving_license
senderIdNumberstringYesSender’s ID number
senderNationalitystringYesSender’s nationality (2-letter country code)
senderTelephoneNostringYesSender’s phone number with country code
receiverFirstNamestringYesReceiver’s first name
receiverLastNamestringYesReceiver’s last name
receiverLipaNumberstringYesPaybill or Till number
receiverAccountstringConditionalPaybill account/reference (required for paybill, omit for till)
relationshipstringYesRelationship to receiver (e.g., “Family”, “Business”)
payoutCountrystringYesDestination country code (KE, TZ, UG, ZA)
purposeOfTransferstringYesPurpose (see allowed values below)
sourceOfFundsstringYesSource of funds (see allowed values below)
businessReferencestringYesYour unique transaction reference
callbackUrlstringNoAbsolute callback URL for this payout’s final status
forceStatusstringTest onlyForce a TESTENV outcome (see Test Payout Outcomes)

Example Request - Till Payment

curl -X POST https://api.test.wakapay.io/business/payout/payment \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "till", "senderCurrency": "USD", "receiverCurrency": "KES", "amount": 100.0, "senderFirstName": "Alice", "senderLastName": "Smith", "senderDob": "1985-03-15", "senderIdType": "passport", "senderIdNumber": "AB1234567", "senderNationality": "TZ", "senderTelephoneNo": "+255712345678", "receiverFirstName": "John", "receiverLastName": "Doe", "receiverLipaNumber": "888880", "relationship": "Family", "payoutCountry": "KE", "purposeOfTransfer": "family_support", "sourceOfFunds": "salary", "businessReference": "TEST-PAYMENT-KE-TILL-001" }'
const response = await fetch( "https://api.test.wakapay.io/business/payout/payment", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "till", senderCurrency: "USD", receiverCurrency: "KES", amount: 100.0, senderFirstName: "Alice", senderLastName: "Smith", senderDob: "1985-03-15", senderIdType: "passport", senderIdNumber: "AB1234567", senderNationality: "TZ", senderTelephoneNo: "+255712345678", receiverFirstName: "John", receiverLastName: "Doe", receiverLipaNumber: "888880", relationship: "Family", payoutCountry: "KE", purposeOfTransfer: "family_support", sourceOfFunds: "salary", businessReference: "TEST-PAYMENT-KE-TILL-001", }), }, ) const data = await response.json() console.log(data)

Till Response (201 Created)

{ "businessReference": "TEST-PAYMENT-KE-TILL-001", "receiverAmount": 100, "receiverCurrency": "KES", "senderAmount": 0.7751937984496124, "senderCurrency": "USD", "status": "termination_pending", "totalDebited": 0.7906976744186047, "wakapayReference": "3454311f-364b-11f1-8c14-0242ac120008" }

Example Request - Paybill Payment

curl -X POST https://api.test.wakapay.io/business/payout/payment \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "paybill", "senderCurrency": "USD", "receiverCurrency": "KES", "amount": 100.0, "senderFirstName": "Alice", "senderLastName": "Smith", "senderDob": "1985-03-15", "senderIdType": "passport", "senderIdNumber": "AB1234567", "senderNationality": "TZ", "senderTelephoneNo": "+255712345678", "receiverFirstName": "John", "receiverLastName": "Doe", "receiverLipaNumber": "123456", "receiverAccount": "ABC123", "relationship": "Family", "payoutCountry": "KE", "purposeOfTransfer": "family_support", "sourceOfFunds": "salary", "businessReference": "TEST-PAYMENT-KE-PAYBILL-001" }'

Response - Paybill (201 Created)

{ "businessReference": "TEST-PAYMENT-KE-PAYBILL-001", "receiverAmount": 100, "receiverCurrency": "KES", "senderAmount": 0.7751937984496124, "senderCurrency": "USD", "status": "termination_pending", "totalDebited": 0.7906976744186047, "wakapayReference": "3487eae9-364b-11f1-8c14-0242ac120008" }

Uganda Till (MTN MoMo Pay) Payouts

Pay Ugandan merchant tills through MTN MoMo Pay. Uganda supports type: "till" only; paybill payments are not supported.

TESTENV UAT Till

FieldValue
Till800001
NameKampala Spices

1. Decode the QR Code (Optional)

Decode the merchant QR string to retrieve its till number.

POST /business/decode-qr
{ "qrString": "800001@momopay:undefined:undefined:256700000001:Kampala Spices", "country": "UG" }

You may omit the country hint. When provided, either country or countryCode can be used.

Decode Response

{ "countryCode": "UG", "currency": "UGX", "merchantName": "Kampala Spices", "channel": "MTN", "tillNumber": "800001", "merchantMobile": "+256700000001", "amount": null }

Use the returned tillNumber as lipaNumber when verifying the till and as receiverLipaNumber when creating the payout.

2. Verify the Till

POST /business/verify-payment
{ "countryCode": "UG", "type": "till", "lipaNumber": "800001" }

Verification Response

{ "verified": true, "displayName": "Kampala Spices" }

3. Create the Payout

POST /business/payout/payment
{ "type": "till", "receiverLipaNumber": "800001", "amount": 1000, "senderCurrency": "USD", "receiverCurrency": "UGX", "payoutCountry": "UG", "businessReference": "UG-TILL-001", "purposeOfTransfer": "business_payment", "sourceOfFunds": "business_income", "senderFirstName": "Acme", "senderLastName": "Ltd", "senderDob": "1990-01-15", "senderIdType": "passport", "senderIdNumber": "P1234567", "senderNationality": "TZ", "senderTelephoneNo": "+255712345678", "receiverFirstName": "Kampala", "receiverLastName": "Spices" }

Uganda Till Rules

RuleDetail
Minimum amount500 UGX
typeMust be "till"
receiverCurrencyMust be "UGX"
payoutCountryMust be "UG"
Receiver namesOptional. If omitted, the API validates the till with Xente and fills in the merchant name.

Bank Payouts

Get Banks

Retrieve the supported banks for a payout country. Use the returned bank code as receiverBankCode when creating a bank payout.

GET /business/banks?countryCode={countryCode}
Query parameterTypeRequiredDescription
countryCodestringYesBank country code. Use KE or ZA
curl "https://api.test.wakapay.io/business/banks?countryCode=KE" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json"

Create a Bank Payout

POST /business/payout/bank
FieldTypeRequiredDescription
senderCurrencystringYesSender’s currency code
receiverCurrencystringYesKES for Kenya or ZAR for South Africa
amountnumberYesAmount to send in receiver’s currency
payoutCountrystringYesKE or ZA
businessReferencestringYesYour unique transaction reference
senderTelephoneNostringYesSender’s phone number with country code
senderFirstNamestringYesSender’s first name
senderLastNamestringYesSender’s last name
senderDobstringYesSender’s date of birth (YYYY-MM-DD)
senderIdTypestringYespassport, national_id, or driving_license
senderIdNumberstringYesSender’s identity document number
senderNationalitystringYesSender’s two-letter nationality code
receiverFirstNamestringYesReceiver’s first name
receiverLastNamestringYesReceiver’s last name
receiverBankCodestringYesBank code returned by the banks endpoint
receiverAccountNumberstringYesReceiver’s bank account number
receiverEmailstringConditionalRequired for South African (ZA) bank payouts
relationshipstringYesSender’s relationship to the receiver
purposeOfTransferstringYesPurpose of transfer
sourceOfFundsstringYesSource of funds
callbackUrlstringNoAbsolute callback URL for this payout’s final status

Kenya Bank Payout

curl -X POST https://api.test.wakapay.io/business/payout/bank \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "senderCurrency": "USD", "receiverCurrency": "KES", "amount": 5000, "payoutCountry": "KE", "businessReference": "PAY-KE-2026-00001", "senderTelephoneNo": "+255712345678", "senderFirstName": "Nate", "senderLastName": "Smith", "senderDob": "1990-01-01", "senderIdType": "passport", "senderIdNumber": "AB1234567", "senderNationality": "TZ", "receiverFirstName": "Jane", "receiverLastName": "Wanjiku", "receiverBankCode": "320", "receiverAccountNumber": "1234567890", "relationship": "Business", "purposeOfTransfer": "business_payment", "sourceOfFunds": "business_income", "callbackUrl": "https://partner.example.com/webhooks/wakapay/PAY-KE-2026-00001" }'
{ "wakapayReference": "c3d4e5f6-a7b8-9012-cdef-123456789012", "businessReference": "PAY-KE-2026-00001", "status": "termination_pending", "senderCurrency": "USD", "receiverCurrency": "KES", "senderAmount": 38.46, "receiverAmount": 5000, "totalDebited": 40.46 }

South Africa Bank Payout

South African bank payouts require receiverEmail.

curl -X POST https://api.test.wakapay.io/business/payout/bank \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "senderCurrency": "USD", "receiverCurrency": "ZAR", "amount": 2000, "payoutCountry": "ZA", "businessReference": "PAY-ZA-2026-00001", "senderTelephoneNo": "+255712345678", "senderFirstName": "Nate", "senderLastName": "Smith", "senderDob": "1990-01-01", "senderIdType": "passport", "senderIdNumber": "AB1234567", "senderNationality": "TZ", "receiverFirstName": "John", "receiverLastName": "Smith", "receiverBankCode": "940044", "receiverAccountNumber": "9876543210", "receiverEmail": "uat.receiver@example.com", "relationship": "Business", "purposeOfTransfer": "business_payment", "sourceOfFunds": "business_income", "callbackUrl": "https://partner.example.com/webhooks/wakapay/PAY-ZA-2026-00001" }'
{ "wakapayReference": "d4e5f6a7-b8c9-0123-def0-234567890123", "businessReference": "PAY-ZA-2026-00001", "status": "termination_pending", "senderCurrency": "USD", "receiverCurrency": "ZAR", "senderAmount": 120.48, "receiverAmount": 2000, "totalDebited": 122.48 }

Transaction Status Values

StatusDescription
termination_pendingTransaction initiated, awaiting completion
termination_successTransaction completed successfully
termination_failureTransaction failed

Test Payout Outcomes

Use forceStatus on POST /business/payout/transfer or POST /business/payout/payment to test specific outcomes.

ValueBehavior
termination_failureReturns a pending payout, then sends a termination_failure webhook
insufficient_balanceReturns 412 immediately and does not create a payout
termination_successReturns a pending payout, then sends a termination_success webhook

Omitting forceStatus follows the normal TESTENV success flow. Use a unique businessReference for every test.

curl -X POST https://api.test.wakapay.io/business/payout/transfer \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "forceStatus": "termination_failure", "senderCurrency": "USD", "receiverCurrency": "KES", "amount": 1000, "senderFirstName": "Test", "senderLastName": "Sender", "senderDob": "1990-01-01", "senderIdType": "passport", "senderIdNumber": "TEST123456", "senderNationality": "US", "senderTelephoneNo": "+12025550100", "receiverFirstName": "Amina", "receiverLastName": "Mjema", "receiverPhone": "+254700000001", "relationship": "Business", "payoutCountry": "KE", "purposeOfTransfer": "family_support", "sourceOfFunds": "salary", "businessReference": "TEST-FAIL-2026-0001", "callbackUrl": "https://partner.example.com/webhooks/wakapay/TEST-FAIL-2026-0001" }'

Purpose of Transfer - Allowed Values

All valid values for the purposeOfTransfer field:

  • family_support - Family support and remittances
  • education - Educational expenses
  • business_payment - Business-related payments
  • medical - Medical expenses
  • investment - Investment purposes
  • construction - Construction expenses
  • rent - Rent and housing
  • general - General purposes
  • fuel - Fuel and energy
  • repairs - Repairs and maintenance
  • gift - Gifts
  • personal_care - Personal care expenses
  • food_and_groceries - Food and groceries
  • transport - Transportation
  • travel - Travel expenses
  • shopping - Shopping
  • entertainment - Entertainment
  • donations - Charitable donations
  • other - Other purposes

Source of Funds - Allowed Values

All valid values for the sourceOfFunds field:

  • salary - Employment salary
  • savings - Personal savings
  • business_income - Business income
  • sale_of_assets - Sale of assets
  • investment_income - Investment returns
  • other - Other sources

TESTENV Test Credentials

Test Phone Numbers (Mobile Money)

CountryPhone NumberStatusDisplay Name
Kenya (KE)+254700000001Verified, payout successAmina Mjema
Tanzania (TZ)+255700000001Verified, no FX rate configuredAmina Mjema
Uganda (UG)+256700000001Verified test recipientAmina Mjema

Test Lipa Numbers (Till/Paybill)

CountryTypeNumberAccount RefStatus
Kenya (KE)Till888880-Verified, payout success
Kenya (KE)Paybill123456ABC123Verified, payout success
Tanzania (TZ)Till600000-Verified, no FX rate
Tanzania (TZ)Paybill700000ABC123Verified, no FX rate

Test Bank Accounts

CountryBank codeAccountEmailAccount name
Kenya (KE)3201234567890Not requiredJane Wanjiku
South Africa (ZA)9400449876543210Any valid email, such as uat.receiver@example.comJohn Smith

Note: Only USD→KES FX rate is configured in TESTENV. TZS payouts will fail with “rate USD_TZS not configured” error.

Error Responses

400 - Invalid Callback URL

{ "code": 0, "error": "callbackUrl must be a valid absolute URL" }

400 - Missing Required Field

{ "code": 0, "error": "receiverPhone is required" }
{ "code": 0, "error": "receiverLipaNumber is required" }

400 - Invalid purposeOfTransfer

{ "code": 0, "error": "purposeOfTransfer must be one of: [family_support, education, business_payment, medical, investment, construction, rent, general, fuel, repairs, gift, personal_care, food_and_groceries, transport, travel, shopping, entertainment, donations, other]" }

400 - Invalid sourceOfFunds

{ "code": 0, "error": "sourceOfFunds must be one of: [salary, savings, business_income, sale_of_assets, investment_income, other]" }

404 - FX Rate Not Configured

{ "code": 0, "error": "rate USD_TZS not configured" }

409 - Duplicate Reference

{ "code": 0, "error": "duplicate reference" }

412 - Insufficient Balance

{ "code": 0, "error": "insufficient available balance" }

In TESTENV, set forceStatus to insufficient_balance to receive this response without creating a payout.

422 - Account Not Active

{ "code": 0, "error": "account not active" }

Note: Non-test phone numbers in TESTENV will return this error.

Use Cases

Send Mobile Money Transfer

async function sendMobileMoneyTransfer(phone, amount, currency, countryCode) { // Step 1: Verify recipient const verifyResponse = await fetch( "https://api.test.wakapay.io/business/verify-transfer", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ countryCode: countryCode, phoneNumber: phone, }), }, ) const verifyData = await verifyResponse.json() if (!verifyData.verified) { throw new Error("Recipient not verified") } console.log(`Sending to: ${verifyData.displayName}`) // Step 2: Check FX rate const rateResponse = await fetch( `https://api.test.wakapay.io/business/rate?from=USD&to=${currency}`, { headers: { Authorization: `Bearer ${token}` }, }, ) const rateData = await rateResponse.json() console.log(`FX Rate: ${rateData.rate}`) // Step 3: Send transfer const transferResponse = await fetch( "https://api.test.wakapay.io/business/payout/transfer", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ senderCurrency: "USD", receiverCurrency: currency, amount: amount, senderFirstName: "Alice", senderLastName: "Smith", senderDob: "1985-03-15", senderIdType: "passport", senderIdNumber: "AB1234567", senderNationality: "TZ", senderTelephoneNo: "+255712345678", receiverFirstName: verifyData.displayName.split(" ")[0], receiverLastName: verifyData.displayName.split(" ")[1] || "User", receiverPhone: phone, relationship: "Family", payoutCountry: countryCode, purposeOfTransfer: "family_support", sourceOfFunds: "salary", businessReference: `TRF-${Date.now()}`, }), }, ) return await transferResponse.json() } // Usage await sendMobileMoneyTransfer("+254700000001", 100, "KES", "KE")

Send Till Payment

async function sendTillPayment(tillNumber, amount, currency, countryCode) { // Step 1: Verify till number const verifyResponse = await fetch( "https://api.test.wakapay.io/business/verify-payment", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ countryCode: countryCode, type: "till", lipaNumber: tillNumber, }), }, ) const verifyData = await verifyResponse.json() if (!verifyData.verified) { throw new Error("Till number not verified") } console.log(`Paying to: ${verifyData.displayName}`) // Step 2: Send payment const paymentResponse = await fetch( "https://api.test.wakapay.io/business/payout/payment", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "till", senderCurrency: "USD", receiverCurrency: currency, amount: amount, senderFirstName: "Alice", senderLastName: "Smith", senderDob: "1985-03-15", senderIdType: "passport", senderIdNumber: "AB1234567", senderNationality: "TZ", senderTelephoneNo: "+255712345678", receiverFirstName: "Merchant", receiverLastName: "Name", receiverLipaNumber: tillNumber, relationship: "Business", payoutCountry: countryCode, purposeOfTransfer: "business_payment", sourceOfFunds: "business_income", businessReference: `TILL-${Date.now()}`, }), }, ) return await paymentResponse.json() } // Usage await sendTillPayment("888880", 100, "KES", "KE")

Best Practices

  1. Always use unique references - Prevents duplicate payments (409 error)
  2. Verify recipients first - Use verify-transfer or verify-payment endpoints
  3. Check FX rates - Ensure currency pair is configured before cross-currency payouts
  4. Display verification results - Show displayName to user for confirmation
  5. Handle errors gracefully - Implement proper error handling for all error codes
  6. Use TESTENV numbers for testing - Use provided test credentials in test environment
  7. Include receiverAccount for paybill - Required field for paybill payments

Supported Countries

  • KE - Kenya
  • TZ - Tanzania
  • UG - Uganda
  • ZA - South Africa
Last updated on