Overview
OTC Exchanges let you convert between EUR and USDC in both directions, settling against your own Balansas resources:- Buy (
EUR → USDC) — pay from one of your EUR fiat accounts and receive into one of your custodial wallets. - Sell (
USDC → EUR) — pay from one of your wallets and receive into one of your EUR fiat accounts.
side parameter ("buy" or "sell") on both
POST /otc-exchanges/quote and POST /otc-exchanges. When side is omitted it
defaults to "buy".
Every order is manually approved by Balansas. The rate on a quote is
indicative — the final executed rate is set at operator approval against
live market pricing. All responses use internal local UUIDs only; vendor
identifiers stay server-side.
Authentication & scopes
| Scope | Grants |
|---|---|
otc-exchanges:read | all GET routes (list, detail, /accounts, /wallets) |
otc-exchanges:write | all POST routes (quote, confirm, cancel) |
- Auth is a session JWT or an
x-api-key. Pay-from and receive-into resources are validated server-side against your own customer — you can only transact with resources you own. POST /otc-exchanges(confirm) requires anIdempotency-Keyheader (UUID v4). A request without it is rejected with400 IDEMPOTENCY_KEY_REQUIRED; repeating a call with the same key replays the original stored response and never acts twice.- The selectors serve both directions:
/otc-exchanges/accountsis the buy source and the sell destination;/otc-exchanges/walletsis the buy destination and the sell source.
The flow
List your eligible resources
GET /otc-exchanges/accounts (your active EUR fiat accounts) and
GET /otc-exchanges/wallets (your non-treasury wallets with a USDC address)
populate the source/destination selectors.Request an indicative quote
POST /otc-exchanges/quote with the side, the source/destination ids, and
the source amount. You get a short-lived quoteId (it is the order id) and
an indicative rate.Confirm the quote into an order
POST /otc-exchanges with the quoteId and an Idempotency-Key header.
A buy debits your EUR to the Balansas treasury immediately; a sell
moves no funds yet.List eligible accounts (Pay-from / Receive-into EUR)
GET /otc-exchanges/accounts · scope otc-exchanges:read
Your own active EUR fiat accounts — the source for a buy and the
destination for a sell.
Response
List eligible wallets (Receive-into / Pay-from USDC)
GET /otc-exchanges/wallets · scope otc-exchanges:read
Your own non-treasury wallets that have a USDC address — the destination for
a buy and the source for a sell. Each wallet includes its chosen USDC
network.
Response
Wallet balance
GET /otc-exchanges/wallets/{id}/balance · scope otc-exchanges:read
Returns the live USDC balance for one of your wallets on its chosen network.
usdcBalance is a decimal string with 6 decimal places.
Response
| Status | Code | Meaning |
|---|---|---|
404 | WALLET_NOT_FOUND | The wallet is not yours or does not exist. |
502 | BALANCE_UNAVAILABLE | The live balance could not be fetched from the upstream provider. |
Request a quote
POST /otc-exchanges/quote · scope otc-exchanges:write
Creates a quoted order and returns a short-lived, indicative quote. The
returned quoteId is the order id — pass it to the confirm step.
Buy (EUR → USDC)
side defaults to "buy" when omitted.
Response (200)
422 with a clear code: SOURCE_NOT_FOUND,
SOURCE_WRONG_CURRENCY, SOURCE_INACTIVE, DESTINATION_NOT_FOUND, or
DESTINATION_NO_USDC_ADDRESS.
Sell (USDC → EUR)
Pay from a wallet, receive into a EUR account.side is required.
Request body
Response (200)
422 with a clear code: SOURCE_WALLET_NOT_FOUND,
SOURCE_WALLET_NO_USDC, SOURCE_WALLET_INSUFFICIENT,
DESTINATION_ACCOUNT_NOT_FOUND, DESTINATION_ACCOUNT_WRONG_CURRENCY, or
DESTINATION_ACCOUNT_INACTIVE. If the live wallet balance can’t be fetched, the
quote returns 502 QUOTE_UNAVAILABLE.
Confirm a quote into an order
POST /otc-exchanges · scope otc-exchanges:write
Pass the same side used on the quote (defaults to "buy").
Buy
Request body
Response (201)
Sell
Request body
pending_approval; the
on-chain USDC debit happens at operator approval.
Response (201)
400 QUOTE_EXPIRED; an order you don’t own
returns 404 ORDER_NOT_FOUND.
List your orders
GET /otc-exchanges · scope otc-exchanges:read — newest first, paginated via
?page and ?limit.
Response
Get a single order
GET /otc-exchanges/{id} · scope otc-exchanges:read — returns 404 for an
order owned by another customer.
Response
Cancel an order (pre-debit only)
POST /otc-exchanges/{id}/cancel · scope otc-exchanges:write
Response (200)
quoted/requoted (truly
pre-execution) orders can be cancelled here. Once an order has moved on
(debit_pending or pending_approval and beyond) the endpoint returns
409 NOT_CANCELLABLE — for a buy your EUR has already moved, and for a sell the
order is already queued for operator approval, so unwinding is handled by
Balansas operations (operator reject), not a customer self-cancel.
Track an order through to settlement via the order list / detail above or with
Webhooks. Because the final rate is set at approval, the
rate and destinationAmount on a completed order may differ from the
indicative quote.
