Pay out to a wallet or a bank account
A payout spends the organisation's fiat balance. The balance a payout can draw on is available_to_convert, which GET /v1/accounts returns as a decimal string on each account. A payout that asks for more than that is refused with insufficient_funds.
A payout names the balance as its source with source.type set to fiat_balance, and a counterparty as its destination. A bank account counterparty receives fiat over the wire rail. A wallet counterparty receives tokens on Stellar. All amounts are decimal strings. This guide uses Testnet only.
Pay a bank account
The three requests below run in CI against an in-process core on FakeChain. First create a bank account counterparty.
POST /v1/counterparties
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{
"type": "bank_account",
"label": "Payout account",
"beneficiary_name": "Acme Testnet Ltd",
"bank_name": "Testnet mock bank",
"account_number": "GB00TEST12345678",
"routing_code": "TEST0001",
"currency": "USD"
}
# expect 201
# save COUNTERPARTY_ID = $.id
Next fund the fiat balance. A wire quoting your organisation's deposit route reference credits it. Find your route's reference with GET /v1/deposit-routes or in the console; the example uses DEP-1000. The sandbox simulates the wire with POST /v1/sandbox/fiat-deposits.
POST /v1/sandbox/fiat-deposits
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{
"reference": "DEP-1000",
"amount": "250.00",
"currency": "USD"
}
# expect 201
Then pay. POST /v1/transfers with a fiat_balance source and a bank destination creates a transfer of kind payout.
POST /v1/transfers
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{
"source": { "type": "fiat_balance", "currency": "USD" },
"destination": { "type": "fiat", "currency": "USD", "rail": "wire", "counterparty_id": "{{COUNTERPARTY_ID}}" },
"amount": "25.00"
}
# expect 201
The bank account's currency must match the source currency. On Testnet the mock bank pays it and the transfer completes with a receipt. Read the transfer back with GET /v1/transfers/{id}.
Pay a wallet
To pay a wallet, set the destination to { "type": "stellar", "asset": "USDX", "counterparty_id": "cpty_..." }. The wallet counterparty must be verified and active, and its account must exist and hold an authorized trust line for the asset. A contract (C-address) wallet can receive a single payout. It cannot be a row in a batch.
POST /v1/transfers
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{
"source": { "type": "fiat_balance", "currency": "USD" },
"destination": { "type": "stellar", "asset": "USDX", "counterparty_id": "cpty_..." },
"amount": "40.00"
}
A wallet payout is refused before anything is created when the destination cannot take it: destination_frozen (422) when the trust line is frozen, destination_no_trustline when it is missing, and state_conflict (409) when the counterparty is not yet verified.
States to handle
A payout starts in created, moves to payout_initiated while a bank payment is in flight (or is submitted on chain for a wallet), and ends completed or failed. Always read status_reason on a transfer that did not complete: it says why. Subscribe to webhooks rather than polling.
Pay many wallets in a batch
POST /v1/payout-batches pays up to 100 wallets from the fiat balance in one request. Send the rows as csv (a counterparty_id,amount header and one line per payment) or as rows, not both.
POST /v1/payout-batches
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{
"source": { "type": "fiat_balance", "currency": "USD" },
"asset": "USDX",
"csv": "counterparty_id,amount\ncpty_...,10.00\ncpty_...,12.50"
}
Batches pay wallets only. A bank account counterparty cannot be a row.
The validation report
Every row is pre-checked when the batch is created. GET /v1/payout-batches/{id}/items returns one item per row with its row, its line in the CSV, a status and, for a row that cannot be paid, a status_reason: for example counterparty_not_found, counterparty_not_wallet, duplicate_row, destination_contract_not_supported or destination_account_missing. Invalid rows are reported and not paid. Read the batch itself with GET /v1/payout-batches/{id}; its counts summarise the rows.
Why rows are pre-checked
A batch is paid in one Stellar transaction, so it succeeds or fails as a whole: every row shares one transaction hash. One bad destination would fail all the others, so a row that fails a pre-check is left out instead.
Retry
When rows fail, create a retry batch that pays only those rows. Send retry_of with the original batch id and nothing else:
POST /v1/payout-batches
authorization: Bearer {{KEY}}
idempotency-key: {{uuid}}
content-type: application/json
{
"retry_of": "bat_..."
}
The new batch pays the failed rows again, and each item records which row of which batch it retries.