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:

ModeVA ModeTriggerDescription
Fiat PayoutfiatNo payment_instructionsDebits the VA balance and initiates a wire/SWIFT transfer
Crypto PayoutcryptoWith payment_instructionsCreates 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 WIRE or SWIFT must exist before initiating a payout

Prerequisites

Before initiating a payout:

  1. Virtual account must be active - Status must be active
  2. Virtual account type must be US_BANK
  3. Recipient must exist - Create a WIRE or SWIFT recipient first
  4. Fiat mode: Sufficient balance to cover amount plus fees
  5. Fiat mode: OTP verification is required (see OTP Verification below)
  6. 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

  1. Request OTP - Call POST /verification/send with the client_uuid or email to receive a 6-digit code via email
  2. Create payout - Include the OTP code in the x-validation-header header when calling POST /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 StatusCodeDescription
400VALIDATION_REQUIREDx-validation-header header is missing for a fiat payout
401OTP_INVALIDOTP code is incorrect or expired. Response includes attemptsRemaining and isBlocked
500OTP_VERIFICATION_ERRORInternal 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

HeaderRequiredDescription
AuthorizationYesBearer token from authentication
x-api-keyYesYour API key
Content-TypeYesMust 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

HeaderRequiredDescription
AuthorizationYesBearer token from authentication
x-api-keyYesYour API key
Content-TypeYesMust be application/json
idempotency-keyYesUnique UUID for idempotent requests
x-validation-headerFiat mode only6-digit OTP code required for fiat (balance) payouts. Obtain via POST /verification/send. Not required for crypto payouts.

Body

FieldTypeRequiredDescription
amountstringYesAmount in USD to send (up to 2 decimals, must be > 0)
recipient_idstring (UUID)YesUUID of the recipient (must be type WIRE or SWIFT)
payment_instructionsobjectConditionalRequired for crypto mode VA, must be absent for fiat mode VA
payment_instructions.networkstringConditionalBlockchain network: solana or polygon
payment_instructions.currencystringConditionalStablecoin: USDC or USDT
supporting_documentsarrayConditionalRequired for crypto mode. Array of supporting documents
supporting_documents[].typestringYesDocument type: invoice or other
supporting_documents[].filestringYesBase64 data URI (PDF, PNG, or JPG)
supporting_documents[].descriptionstringNoDescription (max 255 characters)
client_markupobjectNoAdditional fee on top of Kira's base fees, kept by the client
client_markup.fixed_feestringNoFixed markup in USD (up to 2 decimals)
client_markup.percentage_feestringNoPercentage markup as decimal (e.g., "0.01" = 1%)
client_markup.fx_markupstringNoFX markup rate
extra_infoobjectNoOptional metadata for the payout
extra_info.memostringNoPayment memo or note (max 500 chars)
extra_info.invoice_numberstringNoInvoice reference number (max 100 chars)
extra_info.internal_notesstringNoInternal notes, not shared with recipient (max 1000 chars)

Mode Cross-Validation

The system enforces strict mode consistency:

VA Modepayment_instructionsResult
fiatAbsentFiat payout (debits balance)
fiatPresentError - payment_instructions cannot be provided for fiat mode virtual accounts
cryptoPresentCrypto payout (creates deposit wallet)
cryptoAbsentError - payment_instructions is required for crypto mode virtual accounts

Fiat Payout

How It Works

  1. Preview fees - Call preview endpoint to show fee breakdown
  2. Request OTP - Call POST /verification/send to receive a 6-digit code via email
  3. Create payout - Include OTP in x-validation-header. System validates balance, calculates fees, debits VA
  4. Wire transfer initiated - Banking partner sends WIRE/SWIFT to recipient
  5. 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-header is required for fiat payouts and must contain a valid OTP code obtained from POST /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

  1. Preview fees - Call preview endpoint with payment_instructions
  2. Create payout - System generates a single-use deposit wallet address
  3. Send stablecoins - Caller sends USDC or USDT to the deposit address
  4. Deposit detected - System detects the deposit via blockchain monitoring
  5. Wire transfer initiated - Funds sent to recipient via WIRE/SWIFT
  6. 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.address is 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

FieldTypeModeDescription
idstringBothUnique payout ID
virtual_account_idstringBothThe virtual account ID
recipient_idstringBothThe recipient receiving the funds
amountstringBothPayout amount in USD
currencystringBothSource currency (USD)
statusstringBothPayout status
feesobjectBothFee breakdown
fees.base_feesobjectBothPlatform fees
fees.base_fees.fixed_feestringBothPlatform fixed fee
fees.base_fees.percentage_feestringBothPlatform percentage fee
fees.client_markupobjectBothAdditional markup charged by the client (on top of base fees)
fees.client_markup.fixed_feestringBothClient fixed markup
fees.client_markup.percentage_feestringBothClient percentage markup
fees.total_feesstringBothTotal fees deducted
recipient_amountstringBothAmount the recipient will receive (after fees)
recipient_currencystringBothCurrency the recipient will receive
payment_methodstring | nullBothPayment method derived from recipient account type (e.g. wire, swift). Lowercase.
uetrstring | nullBothUETR (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_instructionsobjectCrypto onlyDeposit wallet details
deposit_instructions.networkstringCrypto onlyBlockchain network
deposit_instructions.currencystringCrypto onlyStablecoin to send
deposit_instructions.addressstringCrypto onlySingle-use wallet address
deposit_instructions.expires_atstringCrypto onlyISO 8601 expiration (24h from creation)
created_atstringBothISO 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:

MethodHow it works
GlobalConfigured by Kira in your fee profile. Applies automatically to all payouts without sending anything in the request.
Per transactionPassed 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_markup is 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

StatusDescription
createdPayout created, awaiting processing (fiat) or deposit (crypto)
pendingPayout queued for execution
processingWire/SWIFT transfer being routed
completedSuccessfully sent to recipient's bank account
failedPayout failed (see error details)
returnedFunds 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

EventWhen
payout.createdPayout created, VA balance debited
payout.status_changedPayout status transition (pending, processing)
payout.completedWire/SWIFT transfer completed
payout.failedPayout failed
payout.returnedFunds returned by recipient bank

Crypto Mode Events

EventWhen
payout.createdPayout created, deposit wallet generated
payout.deposit_receivedStablecoins detected at deposit address
payout.status_changedPayout status transition (pending, processing)
payout.completedWire/SWIFT transfer completed
payout.failedPayout failed at any stage (including expired deposit wallet)
payout.returnedFunds returned by recipient bank

See the Webhooks Guide for full payload structures and event details.


Timing

StageFiat ModeCrypto Mode
Fee calculationInstantInstant
Wallet generationN/AInstant
Deposit detectionN/ADepends on blockchain (seconds to minutes)
Wire processing1-3 business days (domestic)1-3 business days (domestic)
SWIFT processing3-5 business days3-5 business days

Best Practices

General

  1. Always use preview first - Call /payout/preview to show fees before confirmation
  2. Create recipients in advance - Set up WIRE/SWIFT recipients before initiating payouts
  3. Use unique idempotency keys - Generate a new UUID for each payout request
  4. Display fee breakdown - Show all fee components to users for transparency
  5. Listen to webhooks - Use webhooks for real-time status updates
  6. Store payout IDs - Keep track of payout IDs for reconciliation
  7. Handle returns gracefully - Implement logic to handle returned funds

Fiat Mode Specific

  1. Check balance after preview - Verify available balance covers amount plus total_fees
  2. Validate recipient type - Ensure recipient is WIRE or SWIFT before attempting payout
  3. Request OTP just before payout - Call /verification/send right before creating the payout to avoid code expiration

Crypto Mode Specific

  1. Prepare supporting documents - Have invoices or supporting documents ready (required)
  2. Send exact amount - Send the stablecoin amount matching the payout to the deposit address
  3. Respect expiration - Deposit wallet addresses expire after 24 hours
  4. Monitor deposit status - Listen for payout.deposit_received to confirm deposit was detected
  5. Use correct token and network - Must match the payment_instructions specified in the request

Troubleshooting

Payout stuck in created (Fiat)

Possible causes:

  1. High volume of transactions being processed
  2. Banking partner delays

Solutions:

  • Wait for webhook notification
  • Contact support if stuck for more than 24 hours

Payout stuck in created (Crypto)

Possible causes:

  1. Stablecoins not yet sent to deposit address
  2. Wrong token or network used
  3. 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:

  1. Compliance rejection
  2. Invalid recipient details
  3. 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:

  1. Recipient account closed
  2. Recipient bank rejected the transfer
  3. Incorrect account details

Solutions:

  • Verify recipient details and create a new payout
  • Contact recipient to confirm their bank account status

Next Steps

  1. Create recipients for WIRE/SWIFT transfers
  2. Set up webhooks for real-time payout notifications
  3. Check balance before initiating fiat payouts
  4. Review the Webhooks Guide for complete event reference

Support