Payouts
Overview
Payouts allow you to transfer funds from a fiat mode virtual account to a recipient's bank account via WIRE or SWIFT transfer. The payout process sends USD from the virtual account to a pre-configured recipient.
Important:
- Payouts are only available for US_BANK virtual accounts in fiat mode (accounts created without a
destination)- Before initiating a payout, you must create a recipient of type
WIREorSWIFT- Crypto mode accounts automatically convert deposits and do not support manual payouts
- MX_SPEI accounts do not support payouts
Prerequisites
Before initiating a payout:
- Virtual account must be in fiat mode - Created without a
destinationfield - Virtual account type must be US_BANK - MX_SPEI accounts cannot initiate payouts
- Recipient must exist - Create a WIRE or SWIFT recipient first
- Sufficient balance - Available balance must cover the payout amount plus fees
Preview Payout (Get Fees)
Before creating a payout, use the preview endpoint to see the fee breakdown and the amount the recipient will receive.
Endpoint
POST /v1/virtual-accounts/{id}/payout/preview
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer token from authentication |
x-api-key | Yes | Your API key |
Content-Type | Yes | Must be application/json |
Request Body
{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020"
}Example Request
curl -X POST https://api.balampay.com/v1/virtual-accounts/550e8400-e29b-41d4-a716-446655440003/payout/preview \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020"
}'Response
{
"amount": "1000.00",
"currency": "USD",
"fees": {
"base_fees": {
"fixed_fee": 15,
"percentage_fee": 0.5,
"fx_markup": 0
},
"client_markup": {
"fixed_fee": 5,
"percentage_fee": 0,
"fx_markup": 0
},
"total_fees": 25
},
"recipient_amount": "975.00",
"recipient_currency": "USD"
}Fee Components
| Component | Description |
|---|---|
base_fees.fixed_fee | Platform fixed fee |
base_fees.percentage_fee | Platform percentage fee |
base_fees.fx_markup | Platform FX markup (if currency conversion applies) |
client_markup.fixed_fee | Your configured fixed markup |
client_markup.percentage_fee | Your configured percentage markup |
client_markup.fx_markup | Your configured FX markup |
total_fees | Sum of all fees |
Tip: Use the preview endpoint to display fees to users before they confirm the payout.
Create Payout
Endpoint
POST /v1/virtual-accounts/{id}/payout
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer token from authentication |
x-api-key | Yes | Your API key |
Content-Type | Yes | Must be application/json |
idempotency-key | Yes | Unique identifier to prevent duplicate payouts |
Request Body
{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020"
}Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Amount in USD to send (must not exceed available balance minus fees) |
recipient_id | string | Yes | UUID of the recipient (must be type WIRE or SWIFT) |
Example: Payout via WIRE
curl -X POST https://api.balampay.com/v1/virtual-accounts/550e8400-e29b-41d4-a716-446655440003/payout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "idempotency-key: payout-$(uuidgen)" \
-d '{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020"
}'Response
{
"id": "po_550e8400-e29b-41d4-a716-446655440010",
"virtual_account_id": "550e8400-e29b-41d4-a716-446655440003",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020",
"amount": "1000.00",
"currency": "USD",
"status": "pending",
"fees": {
"base_fees": {
"fixed_fee": 15,
"percentage_fee": 0.5,
"fx_markup": 0
},
"client_markup": {
"fixed_fee": 5,
"percentage_fee": 0,
"fx_markup": 0
},
"total_fees": 25
},
"recipient_amount": "975.00",
"recipient_currency": "USD",
"created_at": "2024-01-15T14:30:00Z"
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique payout ID |
virtual_account_id | string | The virtual account ID |
recipient_id | string | The recipient receiving the funds |
amount | string | Amount being sent in USD |
currency | string | Source currency (USD) |
status | string | Payout status |
fees | object | Fee breakdown for the payout |
fees.base_fees | object | Platform fees |
fees.base_fees.fixed_fee | number | Platform fixed fee |
fees.base_fees.percentage_fee | number | Platform percentage fee |
fees.base_fees.fx_markup | number | Platform FX markup |
fees.client_markup | object | Client-configured markup |
fees.client_markup.fixed_fee | number | Client fixed markup |
fees.client_markup.percentage_fee | number | Client percentage markup |
fees.client_markup.fx_markup | number | Client FX markup |
fees.total_fees | number | Total fees deducted |
recipient_amount | string | Amount the recipient will receive (after fees) |
recipient_currency | string | Currency the recipient will receive |
created_at | string | ISO 8601 timestamp of creation |
updated_at | string | ISO 8601 timestamp of last update |
Fee Calculation
Fees are calculated automatically. Use the preview endpoint to see the exact breakdown before confirming a payout.
Example:
Payout Amount: $1,000.00 USD
Base Fees:
Fixed Fee: $15.00
Percentage Fee (0.5%): $5.00
FX Markup: $0.00
Client Markup:
Fixed Fee: $5.00
Percentage Fee: $0.00
FX Markup: $0.00
─────────────────────────────────
Total Fees: $25.00
Recipient Receives: $975.00 USD
Note: Fee configuration varies by client. Contact your account manager for specific fee details.
Payout Status
| Status | Description |
|---|---|
pending | Payout initiated, awaiting processing |
processing | Payout is being routed to the banking partner |
completed | Successfully sent to recipient's bank account |
failed | Payout failed (see error details) |
returned | Funds were returned after being sent |
Status Flow
graph LR
A[pending] --> B[processing]
B --> C[completed]
A --> D[failed]
B --> D
C --> E[returned]
Payout Lifecycle
Step-by-Step Flow
sequenceDiagram
participant Your Server
participant Kira API
participant Banking Partner
participant Recipient Bank
Your Server->>Kira API: POST /payout
Note over Kira API: Validate request
Note over Kira API: Check available balance
Note over Kira API: Calculate fees
Note over Kira API: Reserve funds
Kira API-->>Your Server: 201 Created (status: pending)
Kira API->>Your Server: Webhook: payout.pending
Note over Kira API: Route to banking partner
Kira API->>Your Server: Webhook: payout.processing
Kira API->>Banking Partner: Initiate WIRE/SWIFT transfer
Banking Partner->>Recipient Bank: Send funds
Banking Partner-->>Kira API: Transfer completed
Kira API->>Your Server: Webhook: payout.completed
Timing
| Stage | Duration |
|---|---|
| Request validation | Instant |
| Balance check & fee calculation | Instant |
| WIRE/SWIFT processing | 1-3 business days |
Total time: Typically 1-3 business days for domestic WIRE, 3-5 business days for international SWIFT.
Webhooks
Receive real-time notifications for payout events.
payout.pending
Sent when the payout is created and awaiting processing.
{
"event": "payout.pending",
"data": {
"payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
"virtual_account_id": "550e8400-e29b-41d4-a716-446655440003",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020",
"amount": "1000.00",
"currency": "USD",
"fees": {
"total_fees": 25
},
"recipient_amount": "975.00",
"created_at": "2024-01-15T14:30:00Z"
}
}payout.processing
Sent when the payout is being routed for payment.
{
"event": "payout.processing",
"data": {
"payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
"virtual_account_id": "550e8400-e29b-41d4-a716-446655440003",
"status": "processing",
"created_at": "2024-01-15T14:35:00Z"
}
}payout.completed
Sent when the payout has been successfully sent to the recipient.
{
"event": "payout.completed",
"data": {
"payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
"virtual_account_id": "550e8400-e29b-41d4-a716-446655440003",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020",
"amount": "1000.00",
"currency": "USD",
"fees": {
"total_fees": 25
},
"recipient_amount": "975.00",
"recipient_currency": "USD",
"completed_at": "2024-01-17T10:15:00Z"
}
}payout.failed
Sent when a payout fails.
{
"event": "payout.failed",
"data": {
"payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
"virtual_account_id": "550e8400-e29b-41d4-a716-446655440003",
"amount": "1000.00",
"currency": "USD",
"reason": "compliance_rejected",
"message": "Transaction did not pass compliance review",
"created_at": "2024-01-15T14:35:00Z"
}
}payout.returned
Sent when funds are returned after being sent (e.g., recipient bank rejected the transfer).
{
"event": "payout.returned",
"data": {
"payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
"virtual_account_id": "550e8400-e29b-41d4-a716-446655440003",
"amount": "1000.00",
"currency": "USD",
"reason": "recipient_account_closed",
"message": "Recipient bank account has been closed",
"returned_at": "2024-01-20T09:00:00Z"
}
}Failure Reasons
| Reason | Description |
|---|---|
compliance_rejected | Transaction did not pass compliance review |
insufficient_balance | Account balance became insufficient |
invalid_recipient | Recipient account details are invalid |
recipient_bank_rejected | Recipient's bank rejected the transfer |
recipient_account_closed | Recipient's bank account is closed |
compliance_hold | Transaction held for manual compliance review |
Error Responses
Insufficient Balance
Status: 400 Bad Request
{
"code": "validation_error",
"message": "Insufficient balance. Available: 500.00 USD, Requested: 1000.00 USD (plus 25.00 USD in fees)"
}Invalid Recipient
Status: 400 Bad Request
{
"code": "validation_error",
"message": "Recipient not found or is not of type WIRE or SWIFT"
}Payout on Crypto Mode Account
Status: 400 Bad Request
{
"code": "validation_error",
"message": "Cannot initiate payout from crypto mode virtual account. Deposits are automatically converted and sent to the configured wallet."
}Payout on MX_SPEI Account
Status: 400 Bad Request
{
"code": "validation_error",
"message": "Payouts are only supported for US_BANK fiat mode accounts"
}Virtual Account Not Active
Status: 400 Bad Request
{
"code": "validation_error",
"message": "Virtual account is not active. Current status: deactivated"
}Idempotency Key Conflict
Status: 400 Bad Request
{
"code": "validation_error",
"message": "Idempotency key has already been used with different request data"
}Virtual Account Not Found
Status: 404 Not Found
{
"code": "not_found",
"message": "Virtual account not found: 550e8400-e29b-41d4-a716-446655440003"
}Idempotency
Use the idempotency-key header to safely retry payout requests:
# First request - creates payout
curl -X POST https://api.balampay.com/v1/virtual-accounts/{id}/payout \
-H "idempotency-key: unique-key-123" \
-d '{...}'
# Second request with same key and body - returns existing payout
curl -X POST https://api.balampay.com/v1/virtual-accounts/{id}/payout \
-H "idempotency-key: unique-key-123" \
-d '{...}'Important: If you retry with the same idempotency key but different request data, you will receive a
400error.
Common Scenarios
Scenario 1: Preview Fees Before Payout
// Step 1: Preview to get fees
const previewResponse = await fetch(
`https://api.balampay.com/v1/virtual-accounts/${virtualAccountId}/payout/preview`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'x-api-key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: '1000.00',
recipient_id: recipientId
})
}
);
const preview = await previewResponse.json();
// Display fees to user
console.log(`Amount: $${preview.amount}`);
console.log(`Total Fees: $${preview.fees.total_fees}`);
console.log(`Recipient will receive: $${preview.recipient_amount}`);
// Step 2: Get balance and verify
const balanceResponse = await fetch(
`https://api.balampay.com/v1/virtual-accounts/${virtualAccountId}/balance`,
{
headers: {
'Authorization': `Bearer ${token}`,
'x-api-key': apiKey
}
}
);
const { available_balance } = await balanceResponse.json();
const totalRequired = parseFloat(preview.amount) + preview.fees.total_fees;
if (parseFloat(available_balance) >= totalRequired) {
// Step 3: Execute payout
const payoutResponse = await fetch(
`https://api.balampay.com/v1/virtual-accounts/${virtualAccountId}/payout`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'x-api-key': apiKey,
'Content-Type': 'application/json',
'idempotency-key': crypto.randomUUID()
},
body: JSON.stringify({
amount: preview.amount,
recipient_id: recipientId
})
}
);
const payout = await payoutResponse.json();
console.log(`Payout initiated: ${payout.id}`);
} else {
console.log(`Insufficient balance. Available: ${available_balance}, Required: ${totalRequired}`);
}Scenario 2: Display Fee Breakdown to User
// Use preview to show fees before confirmation
const preview = await previewResponse.json();
const { fees } = preview;
console.log(`
Payout Amount: $${preview.amount}
Base Fees:
Fixed Fee: $${fees.base_fees.fixed_fee}
Percentage Fee: ${fees.base_fees.percentage_fee}%
FX Markup: ${fees.base_fees.fx_markup}%
Client Markup:
Fixed Fee: $${fees.client_markup.fixed_fee}
Percentage Fee: ${fees.client_markup.percentage_fee}%
FX Markup: ${fees.client_markup.fx_markup}%
─────────────────────────────────
Total Fees: $${fees.total_fees}
Recipient Receives: $${preview.recipient_amount}
`);Scenario 3: Handle Webhook Notifications
app.post('/webhooks/kira', (req, res) => {
const event = req.body;
switch (event.event) {
case 'payout.pending':
console.log(`Payout ${event.data.payout_id} created`);
// Update your records, notify user
break;
case 'payout.processing':
console.log(`Payout ${event.data.payout_id} is processing`);
break;
case 'payout.completed':
console.log(`Payout ${event.data.payout_id} completed`);
console.log(`Recipient received: ${event.data.recipient_amount}`);
// Update your records, notify user of success
break;
case 'payout.failed':
console.log(`Payout ${event.data.payout_id} failed`);
console.log(`Reason: ${event.data.reason}`);
// Handle failure, refund balance, notify user
break;
case 'payout.returned':
console.log(`Payout ${event.data.payout_id} was returned`);
console.log(`Reason: ${event.data.reason}`);
// Handle return, credit balance back, notify user
break;
}
res.status(200).send('OK');
});Best Practices
- Always use preview first - Call
/payout/previewto show fees before confirmation - Create recipients in advance - Set up WIRE/SWIFT recipients before initiating payouts
- Check balance after preview - Verify available balance covers amount plus
total_fees - Use unique idempotency keys - Generate a new UUID for each payout request
- Display fee breakdown - Show all fee components to users for transparency
- Listen to webhooks - Don't poll for status; use webhooks for real-time updates
- Store payout IDs - Keep track of payout IDs for reconciliation
- Handle returns gracefully - Implement logic to credit balance back on returns
- Validate recipient type - Ensure recipient is WIRE or SWIFT before attempting payout
Troubleshooting
Payout stuck in pending
Possible causes:
- High volume of transactions being processed
- Banking partner delays
Solutions:
- Wait for webhook notification
- Contact support if pending for more than 24 hours
Payout failed
Possible causes:
- Compliance rejection
- Invalid recipient details
- Recipient bank issues
Solutions:
- Review the failure reason in the webhook
- Verify recipient bank details are correct
- Contact support with payout ID
Funds returned after completion
Possible causes:
- Recipient account closed
- Recipient bank rejected the transfer
- Incorrect account details
Solutions:
- Funds are automatically credited back to virtual account balance
- Verify recipient details and create a new payout
- Contact recipient to confirm their bank account status
Next Steps
- Create recipients for WIRE/SWIFT transfers
- Set up webhooks for real-time payout notifications
- Check balance before initiating payouts
- Build user-facing payout history views
Support
- Email: [email protected]
- Virtual Accounts Guide: virtual-accounts.md
- Deposits Guide: deposits.md
- Webhooks Guide: webhooks.md
Updated 7 days ago
