Payouts
Overview
Payouts allow you to send funds from a virtual account to a recipient's bank account via WIRE or SWIFT transfer. The payout endpoint supports two modes depending on the virtual account configuration:
| Mode | VA Mode | Trigger | Description |
|---|---|---|---|
| Fiat Payout | fiat | No payment_instructions | Debits the VA balance and initiates a wire/SWIFT transfer |
| Crypto Payout | crypto | With payment_instructions | Creates a single-use deposit wallet; after the caller sends stablecoins, the system converts to USD and initiates a wire/SWIFT transfer |
Both modes use the same endpoint (POST /v1/virtual-accounts/{id}/payout) but differ in the request body and response.
Important:
- Only US_BANK virtual accounts in active status support payouts
- A recipient of type
WIREorSWIFTmust exist before initiating a payout
Prerequisites
Before initiating a payout:
- Virtual account must be active - Status must be
active - Virtual account type must be US_BANK
- Recipient must exist - Create a WIRE or SWIFT recipient first
- Fiat mode: Sufficient balance to cover amount plus fees
- Fiat mode: OTP verification is required (see OTP Verification below)
- Crypto mode: Supporting documents are required
OTP Verification (Fiat Mode)
Fiat payouts (from balance) require OTP verification as an additional security layer. Crypto payouts do not require OTP.
Flow
- Request OTP - Call
POST /verification/sendwith theclient_uuidoremailto receive a 6-digit code via email - Create payout - Include the OTP code in the
x-validation-headerheader when callingPOST /v1/virtual-accounts/{id}/payout
Step 1: Request OTP
curl -X POST https://api.balampay.com/verification/send \
-H "Content-Type: application/json" \
-d '{
"client_uuid": "550e8400-e29b-41d4-a716-446655440000"
}'Response:
{
"success": true,
"message": "OTP sent successfully"
}Step 2: Include OTP in Payout Request
Pass the 6-digit code in the x-validation-header header:
curl -X POST https://api.balampay.com/v1/virtual-accounts/{id}/payout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "idempotency-key: $(uuidgen)" \
-H "x-validation-header: 123456" \
-d '{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020"
}'OTP Error Responses
| HTTP Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_REQUIRED | x-validation-header header is missing for a fiat payout |
| 401 | OTP_INVALID | OTP code is incorrect or expired. Response includes attemptsRemaining and isBlocked |
| 500 | OTP_VERIFICATION_ERROR | Internal error verifying OTP |
Example error (invalid OTP):
{
"code": "OTP_INVALID",
"message": "Invalid OTP code",
"attemptsRemaining": 2,
"isBlocked": false
}Note: After too many failed attempts, OTP verification becomes temporarily blocked (
isBlocked: true). Wait before requesting a new code.
Preview Payout
Before creating a payout, use the preview endpoint to see the fee breakdown and the amount the recipient will receive. Both fiat and crypto modes are supported.
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
Fiat Mode Preview
{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020"
}Crypto Mode Preview
{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020",
"payment_instructions": {
"network": "solana",
"currency": "USDC"
}
}Response (Fiat Mode)
{
"amount": "1000.00",
"currency": "USD",
"fees": {
"base_fees": {
"fixed_fee": "15.00",
"percentage_fee": "5.00"
},
"client_markup": {
"fixed_fee": "2.00",
"percentage_fee": "1.00"
},
"total_fees": "23.00"
},
"recipient_amount": "977.00",
"recipient_currency": "USD"
}Response (Crypto Mode)
{
"amount": "1000.00",
"currency": "USD",
"fees": {
"base_fees": {
"fixed_fee": "15.00",
"percentage_fee": "5.00"
},
"client_markup": {
"fixed_fee": "2.00",
"percentage_fee": "1.00"
},
"total_fees": "23.00"
},
"recipient_amount": "977.00",
"recipient_currency": "USD",
"payment_instructions": {
"network": "solana",
"currency": "USDC"
}
}Request Fields
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 UUID for idempotent requests |
x-validation-header | Fiat mode only | 6-digit OTP code required for fiat (balance) payouts. Obtain via POST /verification/send. Not required for crypto payouts. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Amount in USD to send (up to 2 decimals, must be > 0) |
recipient_id | string (UUID) | Yes | UUID of the recipient (must be type WIRE or SWIFT) |
payment_instructions | object | Conditional | Required for crypto mode VA, must be absent for fiat mode VA |
payment_instructions.network | string | Conditional | Blockchain network: solana or polygon |
payment_instructions.currency | string | Conditional | Stablecoin: USDC or USDT |
supporting_documents | array | Conditional | Required for crypto mode. Array of supporting documents |
supporting_documents[].type | string | Yes | Document type: invoice or other |
supporting_documents[].file | string | Yes | Base64 data URI (PDF, PNG, or JPG) |
supporting_documents[].description | string | No | Description (max 255 characters) |
client_markup | object | No | Additional fee on top of Kira's base fees, kept by the client |
client_markup.fixed_fee | string | No | Fixed markup in USD (up to 2 decimals) |
client_markup.percentage_fee | string | No | Percentage markup as decimal (e.g., "0.01" = 1%) |
client_markup.fx_markup | string | No | FX markup rate |
extra_info | object | No | Optional metadata for the payout |
extra_info.memo | string | No | Payment memo or note (max 500 chars) |
extra_info.invoice_number | string | No | Invoice reference number (max 100 chars) |
extra_info.internal_notes | string | No | Internal notes, not shared with recipient (max 1000 chars) |
Mode Cross-Validation
The system enforces strict mode consistency:
| VA Mode | payment_instructions | Result |
|---|---|---|
fiat | Absent | Fiat payout (debits balance) |
fiat | Present | Error - payment_instructions cannot be provided for fiat mode virtual accounts |
crypto | Present | Crypto payout (creates deposit wallet) |
crypto | Absent | Error - payment_instructions is required for crypto mode virtual accounts |
Fiat Payout
How It Works
- Preview fees - Call preview endpoint to show fee breakdown
- Request OTP - Call
POST /verification/sendto receive a 6-digit code via email - Create payout - Include OTP in
x-validation-header. System validates balance, calculates fees, debits VA - Wire transfer initiated - Banking partner sends WIRE/SWIFT to recipient
- Webhook notifications - You receive status updates via webhooks
Example Request
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: $(uuidgen)" \
-H "x-validation-header: 123456" \
-d '{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020"
}'Note: The
x-validation-headeris required for fiat payouts and must contain a valid OTP code obtained fromPOST /verification/send. See OTP Verification.
Response
{
"id": "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": "created",
"fees": {
"base_fees": {
"fixed_fee": "15.00",
"percentage_fee": "5.00"
},
"client_markup": {
"fixed_fee": "2.00",
"percentage_fee": "1.00"
},
"total_fees": "23.00"
},
"recipient_amount": "977.00",
"recipient_currency": "USD",
"payment_method": "wire",
"created_at": "2024-01-15T14:30:00Z"
}Lifecycle
sequenceDiagram
participant Your Server
participant Kira API
participant Banking Partner
participant Recipient Bank
Your Server->>Kira API: POST /payout/preview
Kira API-->>Your Server: Fee breakdown
Your Server->>Kira API: POST /verification/send
Kira API-->>Your Server: OTP sent via email
Your Server->>Kira API: POST /payout (x-validation-header: OTP)
Note over Kira API: Verify OTP
Note over Kira API: Validate balance
Note over Kira API: Calculate fees
Note over Kira API: Debit VA balance
Kira API-->>Your Server: 201 Created (status: created)
Kira API->>Your Server: Webhook: payout.created
Note over Kira API: Queue payout execution
Kira API->>Your Server: Webhook: payout.status_changed (PENDING)
Note over Kira API,Banking Partner: Initiate wire transfer
Kira API->>Banking Partner: Create wire outbound
Kira API->>Your Server: Webhook: payout.status_changed (PROCESSING)
Banking Partner->>Recipient Bank: WIRE/SWIFT transfer
Banking Partner-->>Kira API: Transfer completed
Kira API->>Your Server: Webhook: payout.completed
Crypto Payout
How It Works
- Preview fees - Call preview endpoint with
payment_instructions - Create payout - System generates a single-use deposit wallet address
- Send stablecoins - Caller sends USDC or USDT to the deposit address
- Deposit detected - System detects the deposit via blockchain monitoring
- Wire transfer initiated - Funds sent to recipient via WIRE/SWIFT
- Webhook notifications - You receive status updates throughout the process
Example Request
curl -X POST https://api.balampay.com/v1/virtual-accounts/550e8400-e29b-41d4-a716-446655440001/payout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "idempotency-key: $(uuidgen)" \
-d '{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020",
"payment_instructions": {
"network": "solana",
"currency": "USDC"
},
"supporting_documents": [
{
"type": "invoice",
"file": "data:application/pdf;base64,JVBERi0xLjQK...",
"description": "Invoice #1234"
}
],
"extra_info": {
"memo": "Payment for consulting services",
"invoice_number": "INV-2024-1234"
}
}'Response
{
"id": "550e8400-e29b-41d4-a716-446655440010",
"virtual_account_id": "550e8400-e29b-41d4-a716-446655440001",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020",
"amount": "1000.00",
"currency": "USD",
"status": "created",
"fees": {
"base_fees": {
"fixed_fee": "15.00",
"percentage_fee": "5.00"
},
"client_markup": {
"fixed_fee": "2.00",
"percentage_fee": "1.00"
},
"total_fees": "23.00"
},
"recipient_amount": "977.00",
"recipient_currency": "USD",
"payment_method": "swift",
"deposit_instructions": {
"network": "solana",
"currency": "USDC",
"address": "BaLaMxyz123...",
"expires_at": "2024-01-16T14:30:00Z"
},
"created_at": "2024-01-15T14:30:00Z"
}Important: The
deposit_instructions.addressis a single-use wallet. Send the exact stablecoin amount to this address within the expiration window (24 hours).
Lifecycle
sequenceDiagram
participant Your Server
participant Kira API
participant Blockchain
participant Banking Partner
participant Recipient Bank
Your Server->>Kira API: POST /payout/preview (with payment_instructions)
Kira API-->>Your Server: Fee breakdown
Your Server->>Kira API: POST /payout (with payment_instructions)
Note over Kira API: Generate deposit wallet
Kira API-->>Your Server: 201 Created + deposit_instructions
Kira API->>Your Server: Webhook: payout.created
Note over Your Server,Blockchain: Send stablecoins to deposit address
Your Server->>Blockchain: Transfer stablecoins
Blockchain-->>Kira API: Deposit detected (via QuickNode)
Kira API->>Your Server: Webhook: payout.deposit_received
Kira API->>Your Server: Webhook: payout.status_changed (PENDING)
Kira API->>Banking Partner: Create wire outbound
Kira API->>Your Server: Webhook: payout.status_changed (PROCESSING)
Banking Partner->>Recipient Bank: WIRE/SWIFT transfer
Banking Partner-->>Kira API: Transfer completed
Kira API->>Your Server: Webhook: payout.completed
Response Fields
| Field | Type | Mode | Description |
|---|---|---|---|
id | string | Both | Unique payout ID |
virtual_account_id | string | Both | The virtual account ID |
recipient_id | string | Both | The recipient receiving the funds |
amount | string | Both | Payout amount in USD |
currency | string | Both | Source currency (USD) |
status | string | Both | Payout status |
fees | object | Both | Fee breakdown |
fees.base_fees | object | Both | Platform fees |
fees.base_fees.fixed_fee | string | Both | Platform fixed fee |
fees.base_fees.percentage_fee | string | Both | Platform percentage fee |
fees.client_markup | object | Both | Additional markup charged by the client (on top of base fees) |
fees.client_markup.fixed_fee | string | Both | Client fixed markup |
fees.client_markup.percentage_fee | string | Both | Client percentage markup |
fees.total_fees | string | Both | Total fees deducted |
recipient_amount | string | Both | Amount the recipient will receive (after fees) |
recipient_currency | string | Both | Currency the recipient will receive |
payment_method | string | null | Both | Payment method derived from recipient account type (e.g. wire, swift). Lowercase. |
uetr | string | null | Both | UETR (Unique End-to-End Transaction Reference) for SWIFT transfers. Available once the partner bank acknowledges the wire. null for non-SWIFT payouts or before partner acknowledgement |
deposit_instructions | object | Crypto only | Deposit wallet details |
deposit_instructions.network | string | Crypto only | Blockchain network |
deposit_instructions.currency | string | Crypto only | Stablecoin to send |
deposit_instructions.address | string | Crypto only | Single-use wallet address |
deposit_instructions.expires_at | string | Crypto only | ISO 8601 expiration (24h from creation) |
created_at | string | Both | ISO 8601 timestamp of creation |
Fee Calculation
Fees are calculated from the fee profile configured for your client account. Use the preview endpoint to see the exact breakdown before confirming.
Fee Components
Payout Amount: $1,000.00 USD
Base Fees:
Fixed Fee: $15.00
Percentage Fee (0.5%): $5.00
Client Markup:
Fixed Fee: $2.00
Percentage Fee (0.1%): $1.00
---------------------------------
Total Fees: $23.00
Recipient Receives: $977.00 USD
Client Markup
The client_markup is an additional fee that you charge on top of Kira's base fees. This markup is kept by you and allows you to add your own margin to each payout.
There are two ways to configure it:
| Method | How it works |
|---|---|
| Global | Configured by Kira in your fee profile. Applies automatically to all payouts without sending anything in the request. |
| Per transaction | Passed in the request body. Overrides the global markup for that specific payout. |
Example: Per-transaction markup
{
"amount": "1000.00",
"recipient_id": "550e8400-e29b-41d4-a716-446655440020",
"client_markup": {
"fixed_fee": "10.00",
"percentage_fee": "0.02"
}
}In this example, instead of using the global markup from your fee profile, the payout will apply a $10.00 fixed fee plus a 2% percentage fee as your client markup.
Note: If
client_markupis not included in the request, the global markup from your fee profile is applied automatically.Note: Fee configuration varies by client. Contact your account manager for specific fee details.
Payout Status
| Status | Description |
|---|---|
created | Payout created, awaiting processing (fiat) or deposit (crypto) |
pending | Payout queued for execution |
processing | Wire/SWIFT transfer being routed |
completed | Successfully sent to recipient's bank account |
failed | Payout failed (see error details) |
returned | Funds were returned after being sent |
Status Flow - Fiat Mode
graph LR
A[created] --> B[pending]
B --> C[processing]
C --> D[completed]
B --> E[failed]
C --> E
D --> F[returned]
Status Flow - Crypto Mode
graph LR
A[created] -->|deposit received| B[pending]
B --> C[processing]
C --> D[completed]
A --> E[failed]
B --> E
C --> E
D --> F[returned]
Webhooks
Fiat Mode Events
| Event | When |
|---|---|
payout.created | Payout created, VA balance debited |
payout.status_changed | Payout status transition (pending, processing) |
payout.completed | Wire/SWIFT transfer completed |
payout.failed | Payout failed |
payout.returned | Funds returned by recipient bank |
Crypto Mode Events
| Event | When |
|---|---|
payout.created | Payout created, deposit wallet generated |
payout.deposit_received | Stablecoins detected at deposit address |
payout.status_changed | Payout status transition (pending, processing) |
payout.completed | Wire/SWIFT transfer completed |
payout.failed | Payout failed at any stage (including expired deposit wallet) |
payout.returned | Funds returned by recipient bank |
See the Webhooks Guide for full payload structures and event details.
Timing
| Stage | Fiat Mode | Crypto Mode |
|---|---|---|
| Fee calculation | Instant | Instant |
| Wallet generation | N/A | Instant |
| Deposit detection | N/A | Depends on blockchain (seconds to minutes) |
| Wire processing | 1-3 business days (domestic) | 1-3 business days (domestic) |
| SWIFT processing | 3-5 business days | 3-5 business days |
Best Practices
General
- Always use preview first - Call
/payout/previewto show fees before confirmation - Create recipients in advance - Set up WIRE/SWIFT recipients before initiating payouts
- 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 - Use webhooks for real-time status updates
- Store payout IDs - Keep track of payout IDs for reconciliation
- Handle returns gracefully - Implement logic to handle returned funds
Fiat Mode Specific
- Check balance after preview - Verify available balance covers amount plus
total_fees - Validate recipient type - Ensure recipient is WIRE or SWIFT before attempting payout
- Request OTP just before payout - Call
/verification/sendright before creating the payout to avoid code expiration
Crypto Mode Specific
- Prepare supporting documents - Have invoices or supporting documents ready (required)
- Send exact amount - Send the stablecoin amount matching the payout to the deposit address
- Respect expiration - Deposit wallet addresses expire after 24 hours
- Monitor deposit status - Listen for
payout.deposit_receivedto confirm deposit was detected - Use correct token and network - Must match the
payment_instructionsspecified in the request
Troubleshooting
Payout stuck in created (Fiat)
Possible causes:
- High volume of transactions being processed
- Banking partner delays
Solutions:
- Wait for webhook notification
- Contact support if stuck for more than 24 hours
Payout stuck in created (Crypto)
Possible causes:
- Stablecoins not yet sent to deposit address
- Wrong token or network used
- Deposit address expired
Solutions:
- Verify the deposit was sent to the correct address
- Confirm the correct token and network were used
- Check if the deposit address has expired (24h window)
Payout failed
Possible causes:
- Compliance rejection
- Invalid recipient details
- Internal payment failure (crypto mode)
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:
- 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 fiat payouts
- Review the Webhooks Guide for complete event reference
Support
- Email: [email protected]
- Virtual Accounts Guide: virtual-accounts.md
- Deposits Guide: deposits.md
- Webhooks Guide: webhooks.md
-
Updated 22 days ago
