Skip to content
Testnet only: a reference implementation, not a production service.Tokens and bank transfers here are simulated and have no value.

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.

Edit on GitHub