#Exchange
Sell a coin you hold and pay any of several thousand others out to an address
5 endpoints. Generated from docs/openapi.yaml v1.0.0.
| Endpoint | Summary |
|---|---|
GET /v1/exchange/assets | List the coins an exchange can buy |
GET /v1/exchange/chains | List the chains an exchange can pay out to |
POST /v1/exchange/quote | Price an exchange, and validate the destination address |
POST /v1/exchange/orders | Start an exchange — sell a held coin, pay another one out |
GET /v1/swap/orders | List swap and exchange orders |
#GET/v1/exchange/assets
List the coins an exchange can buy · Auth: bearer API key · operationId: listExchangeAssets
The payout catalogue — thousands of coins across dozens of chains, refreshed hourly from the router; total on this route is the live count. Far wider than GET /v1/assets (the handful a user can HOLD with us). Nothing here is custodial: these are payout destinations only.
asset is the EXCHANGE's own identifier (e.g. SOL.SOL, ETH.USDC-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) and is exactly what buy_asset expects on POST /v1/exchange/quote and POST /v1/exchange/orders. It is NOT one of our asset_id values — take it from here rather than constructing it, because a value we cannot route is rejected outright.
total counts every match; assets is capped at limit (default 100), so total greater than the array's length means "narrow the search", not "there are only this many".
#Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
search | query | string | no | Case-insensitive substring match on ticker, name or identifier. |
chain | query | string | no | Exact chain, case-insensitive (e.g. SOL). Values from GET /v1/exchange/chains. |
limit | query | integer | no | — (default 100; min 1; max 500) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Matching payout assets. |
401 | Error | Missing, malformed or unknown API key. |
403 | Error | Swaps and exchanges are turned off, or the exchange integration is not configured. |
502 | Error | The exchange failed or was unreachable (your request was valid). |
200 response body
| Field | Type | Description |
|---|---|---|
total required | integer | Matches BEFORE limit was applied. |
assets required | object[] | — |
assets[].asset required | string | The EXCHANGE's identifier — what buy_asset expects. NOT one of our asset_id values, and not derivable from one. |
assets[].chain required | string | Chain as the exchange names it — a wider set than our Chain enum. |
assets[].symbol required | string | — |
assets[].name required | string | — |
assets[].decimals required | integer | The coin's own decimals, on a chain we do not model. Informational: we publish no base units for it, so amounts stay decimal strings. |
assets[].contract_address required | string | null | Token contract, or null for a chain's native coin. |
assets[].logo_url required | string | null | A path on this API that serves the coin's picture (/v1/exchange/assets/{identifier}/logo). Null when the coin has none. Fetch it with your API key like any other call; it is cacheable for a week. |
{
"total": 1,
"assets": [
{
"asset": "ETH.USDC-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"chain": "ETH",
"symbol": "USDC",
"name": "USD Coin",
"decimals": 1,
"contract_address": "string",
"logo_url": "string"
}
]
}#Example request
curl -X GET 'https://api.cryptoinfra.build/v1/exchange/assets' \
-H 'Authorization: Bearer ck_live_your_key'#GET/v1/exchange/chains
List the chains an exchange can pay out to · Auth: bearer API key · operationId: listExchangeChains
The distinct chains present in the catalogue, sorted — for a picker's chain filter. Feed a value back as the chain query on GET /v1/exchange/assets.
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Payout chains. |
401 | Error | Missing, malformed or unknown API key. |
403 | Error | Swaps and exchanges are turned off, or the exchange integration is not configured. |
502 | Error | The exchange failed or was unreachable (your request was valid). |
200 response body
| Field | Type | Description |
|---|---|---|
chains required | string[] | — |
{
"chains": [
"SOL"
]
}#Example request
curl -X GET 'https://api.cryptoinfra.build/v1/exchange/chains' \
-H 'Authorization: Bearer ck_live_your_key'#POST/v1/exchange/quote
Price an exchange, and validate the destination address · Auth: bearer API key · operationId: quoteExchange
Indicative: confers no reservation and holds no balance. But it DOES validate destination_address, and that is the reason to always call it first. We have no address library for most of these chains, so the exchange is asked at quote time; a bad address comes back as 400 invalid_pair before anything is charged. Quote before you create, and show the rejection to your customer.
The SELL side carries the usual *_base twin. The BUY side does not: the coin's decimals belong to a chain we do not model, so receive_estimate and receive_minimum are the exchange's own decimal strings. Render them as given — never scale them by an asset of ours.
receive_estimate is an estimate; receive_minimum is the floor this route guarantees. The customer is paid what ACTUALLY ARRIVES, which is why both are published: the pair is the promise.
A quote is good for at most 60 seconds, or less when the route itself expires sooner — expires_at carries the earlier of the two.
destination_address is optional here — omit it to price alone, send it and it is validated too. It is required on POST /v1/exchange/orders.
#Request body
application/json · required · schema ExchangeQuoteRequest
| Field | Type | Description |
|---|---|---|
sell_asset_id required | string | One of OUR assets (GET /v1/assets) — what is sold out of the user's balance. A disabled asset, or one with swappable: false, is refused with 400 invalidpair (the message names it), exactly as on a swap. (minLength 1; maxLength 64)_ |
buy_asset required | string | The coin to buy, as the EXCHANGE's identifier — the asset field of GET /v1/exchange/assets, e.g. SOL.SOL or ETH.USDC-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48. This is NOT one of our asset ids; anything we cannot route is rejected with 400 invalidpair. (minLength 1; maxLength 128)_ |
amount required | string | number | Decimal amount of sell_asset_id, fee INCLUSIVE: our fee comes out of it and send_amount is what reaches the exchange. |
destination_address | string | Where the bought coin is paid out. Must be valid on buy_asset's chain. Optional on a quote — omit it to price alone, send it and the exchange validates it here too. Required on POST /v1/exchange/orders. We have no address library for most of these chains, so the exchange is asked at quote time and an invalid address is refused with 400 invalidpair — before anything is charged. Once an order is created this payout is IRREVERSIBLE: the coin never enters our custody. (minLength 1; maxLength 256)_ |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | ExchangeQuote | An indicative price, with the moment it stops being valid. |
400 | Error | The request was refused before anything was charged. error is one of: validationerror, invalidamount, invalidpair (buy_asset is not routable, the sell asset is disabled or no exchange can route it — swappable: false on GET /v1/assets, the message names it — both sides are the same coin, or destination_address is not valid for buy_asset's chain), noroute (that pair has no liquidity path at this size — nothing is broken, try another amount or coin). |
401 | Error | Missing, malformed or unknown API key. |
403 | Error | Swaps and exchanges are turned off, or the exchange integration is not configured. |
502 | Error | The exchange failed or was unreachable (your request was valid). |
503 | Error | Our side is not ready for this chain. The request was fine — retry rather than change it. |
200 response body — ExchangeQuote
| Field | Type | Description |
|---|---|---|
sell_asset_id required | string | — |
buy_asset required | string | The exchange identifier you asked for, echoed back. |
buy_symbol required | string | — |
buy_chain required | string | — |
sell_amount required | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
sell_amount_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee required | string | Our fee, taken OUT of sell_amount — not charged on top. |
fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
send_amount required | string | sell_amount minus fee — what reaches the exchange, published so the numbers add up. No _base twin here. |
receive_estimate required | string | Estimated payout, as the EXCHANGE's decimal string on its own chain. Render as given; there is no receive_estimate_base for an exchange. |
receive_minimum required | string | The floor this route guarantees, same encoding. The customer is credited what ARRIVES, not this — treat it as the worst case you may quote them. |
quoted_at | string<date-time> | When this quote was priced; expires_at is how long the route stands. |
providers required | string[] | The route's legs, in order. |
estimated_seconds required | integer | null | — |
expires_at required | string<date-time> | When the quote stops being valid. Same shape as the swap quote's. |
{
"sell_asset_id": "string",
"buy_asset": "SOL.SOL",
"buy_symbol": "SOL",
"buy_chain": "SOL",
"sell_amount": "1.5",
"sell_amount_base": "1500000000",
"fee": "1.5",
"fee_base": "1500000000",
"send_amount": "1.5",
"receive_estimate": "0.4193",
"receive_minimum": "0.4109",
"quoted_at": "2026-08-31T09:15:00.000Z",
"providers": [
"string"
],
"estimated_seconds": 1,
"expires_at": "2026-08-31T04:21:00.000Z"
}#Example request
curl -X POST 'https://api.cryptoinfra.build/v1/exchange/quote' \
-H 'Authorization: Bearer ck_live_your_key' \
-H 'Content-Type: application/json' \
-d '{
"sell_asset_id": "usdt_trc20",
"buy_asset": "SOL.SOL",
"amount": "100"
}'#POST/v1/exchange/orders
Start an exchange — sell a held coin, pay another one out · Auth: bearer API key · operationId: createExchangeOrder
201, and the user's balance is ALREADY debited: the sell leg is an ordinary withdrawal, priced, signed and broadcast by the same code as any other payout. Our fee comes out of the amount, and the withdrawal fee is NOT charged on top.
The bought coin is paid to destination_address and NEVER enters our custody. It cannot be reversed and it cannot be recovered — the same finality as a withdrawal, which is exactly what the sell leg is. POST /v1/exchange/quote is the only validation that address gets, and it costs nothing: quote first.
Idempotent on idempotency_key — replaying returns the existing order instead of starting a second one; the same key with a DIFFERENT payload is refused with 409 idempotency_conflict. Then poll GET /v1/swap/orders/{id}: pending until the sell leg is on-chain, sent/swapping while the exchange works, and completed once it reports the payout. Nothing is ever credited to the user's balance; received is what landed at destination_address.
Buy-side amounts (receive_estimate, receive_minimum, received) are the exchange's decimal strings and their *_base twins are null.
#Request body
application/json · required · schema ExchangeOrderRequest
| Field | Type | Description |
|---|---|---|
sell_asset_id required | string | One of OUR assets (GET /v1/assets) — what is sold out of the user's balance. A disabled asset, or one with swappable: false, is refused with 400 invalidpair (the message names it), exactly as on a swap. (minLength 1; maxLength 64)_ |
buy_asset required | string | The coin to buy, as the EXCHANGE's identifier — the asset field of GET /v1/exchange/assets, e.g. SOL.SOL or ETH.USDC-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48. This is NOT one of our asset ids; anything we cannot route is rejected with 400 invalidpair. (minLength 1; maxLength 128)_ |
amount required | string | number | Decimal amount of sell_asset_id, fee INCLUSIVE: our fee comes out of it and send_amount is what reaches the exchange. |
destination_address required | string | Where the bought coin is paid out. Must be valid on buy_asset's chain. Optional on a quote — omit it to price alone, send it and the exchange validates it here too. Required on POST /v1/exchange/orders. We have no address library for most of these chains, so the exchange is asked at quote time and an invalid address is refused with 400 invalidpair — before anything is charged. Once an order is created this payout is IRREVERSIBLE: the coin never enters our custody. (minLength 1; maxLength 256)_ |
user_id required | string | Your external_user_id — whose balance is sold. (minLength 1) |
idempotency_key | string | Replaying a key returns the existing order instead of starting a second one; the same key with a different payload is 409 idempotencyconflict. (minLength 1; maxLength 255)_ |
#Responses
| Status | Body | Meaning |
|---|---|---|
201 | SwapOrder | Exchange started; the sell leg is queued as a withdrawal. |
400 | Error | The request was refused before anything was charged. error is one of: validationerror, invalidamount, invalidpair (buy_asset is not routable, the sell asset is disabled or no exchange can route it — swappable: false on GET /v1/assets, the message names it — both sides are the same coin, or destination_address is not valid for buy_asset's chain), noroute (that pair has no liquidity path at this size — nothing is broken, try another amount or coin). |
401 | Error | Missing, malformed or unknown API key. |
403 | Error | Swaps and exchanges are turned off, or the exchange integration is not configured. |
404 | Error | Unknown externaluserid for this tenant. |
409 | Error | error is one of: insufficientbalance (the sell amount would overdraw the user's balance), idempotencyconflict (this idempotency_key was already used with a DIFFERENT payload — your replay is not the request you think it is). |
502 | Error | The exchange failed or was unreachable (your request was valid). |
503 | Error | Our side is not ready for this chain. The request was fine — retry rather than change it. |
201 response body — SwapOrder
| Field | Type | Description |
|---|---|---|
order_id required | string<uuid> | — |
user_id | string | Your external_user_id. Present on create; absent when listing or fetching. |
kind required | string | swap buys one of OUR assets: the output lands on the user's own deposit address and is credited to their balance. exchange buys a coin we do not custody: the output is paid to destination_address and never enters our system. (one of swap, exchange) |
sell_asset_id required | string | — |
buy_asset_id required | string | null | One of our assets on a swap; null on an exchange (we have no asset row for it). |
buy_asset_external required | string | null | The exchange's identifier on an exchange (e.g. SOL.SOL); null on a swap. |
buy_symbol required | string | null | Display ticker of the bought coin on an exchange, so a listing needs no catalogue lookup. Null on a swap. |
buy_chain required | string | null | Chain of the bought coin on an exchange, as the exchange names it. Null on a swap. |
destination_address | string | Where the bought coin was paid. Present on EXCHANGE rows ONLY — echoed back so you can confirm it — and absent on a swap, whose output goes to the user's own static wallet address. This payout is irreversible and outside our custody. |
sell_amount required | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
sell_amount_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee required | string | Our fee, taken out of sell_amount — a percentage with a dollar minimum (see GET /v1/pricing), plus network_fee. |
fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
network_fee | string | The network part of fee, in the sold asset — a USDT ⇄ TRX trade's own gas on TRON, or an exchange's send. Already inside fee. |
network_fee_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
receive_estimate | string | null | What the route estimated. A swap's is in the buy asset's major units; an exchange's is the exchange's decimal string. |
receive_estimate_base | string | null | Exact base units on a SWAP. Always null on an exchange. |
receive_minimum | string | null | The floor the route guaranteed. The user is credited what arrives, not this. |
receive_minimum_base | string | null | Exact base units on a SWAP. Always null on an exchange. |
received | string | null | What ACTUALLY arrived. Null until the exchange settles. |
received_base | string | null | Exact base units on a SWAP. Always null on an exchange. |
providers required | string[] | — |
status required | string | pending until the sell leg is on-chain, then sent and swapping while the exchange works; completed when it reports the payout. (one of pending, sent, swapping, completed, refunded, failed) |
tracking_status | string | null | The exchange's own last word on the route, verbatim. Informational. |
txid | string | null | The on-chain SELL payment that started the order — the handle to show a customer, and the one the exchange's tracker keys on. Null until it is sent. |
created_at required | string<date-time> | — |
updated_at required | string<date-time> | — |
{
"order_id": "string",
"user_id": "string",
"kind": "swap",
"sell_asset_id": "string",
"buy_asset_id": "string",
"buy_asset_external": "string",
"buy_symbol": "string",
"buy_chain": "string",
"destination_address": "string",
"sell_amount": "1.5",
"sell_amount_base": "1500000000",
"fee": "1.5",
"fee_base": "1500000000",
"network_fee": "1.5",
"network_fee_base": "1500000000",
"receive_estimate": "string",
"receive_estimate_base": "string",
"receive_minimum": "string",
"receive_minimum_base": "string",
"received": "string",
"received_base": "string",
"providers": [
"string"
],
"status": "pending",
"tracking_status": "string",
"txid": "string",
"created_at": "2026-08-31T09:15:00.000Z",
"updated_at": "2026-08-31T09:15:00.000Z"
}#Example request
curl -X POST 'https://api.cryptoinfra.build/v1/exchange/orders' \
-H 'Authorization: Bearer ck_live_your_key' \
-H 'Content-Type: application/json' \
-d '{
"sell_asset_id": "usdt_trc20",
"buy_asset": "SOL.SOL",
"amount": "100",
"destination_address": "string",
"user_id": "string"
}'#GET/v1/swap/orders
List swap and exchange orders · Auth: bearer API key · operationId: listSwapOrders
Newest-first, scoped to your merchant. Swaps and exchanges share ONE order list and one order shape; kind filters between them.
A swap names one of our assets on both sides and credits the user's balance. An exchange names its buy side by the exchange's identifier (buy_asset_external, with buy_symbol and buy_chain so a listing needs no catalogue lookup) and pays out to destination_address, which is echoed on exchange rows only. On an exchange buy_asset_id is null, and every buy-side *_base field is null with it — those decimals belong to a chain we do not model, so there are no base units of ours to publish.
#Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
kind | query | string | no | Return only swaps, or only exchanges. Omit for both. (one of swap, exchange) |
status | query | string | no | — (one of pending, sent, swapping, completed, refunded, failed) |
limit | query | integer | no | — (default 50; min 1; max 200) |
offset | query | integer | no | — (default 0; min 0) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Matching orders. |
401 | Error | Missing, malformed or unknown API key. |
200 response body
| Field | Type | Description |
|---|---|---|
orders required | object[] | — |
orders[].order_id required | string<uuid> | — |
orders[].user_id | string | Your external_user_id. Present on create; absent when listing or fetching. |
orders[].kind required | string | swap buys one of OUR assets: the output lands on the user's own deposit address and is credited to their balance. exchange buys a coin we do not custody: the output is paid to destination_address and never enters our system. (one of swap, exchange) |
orders[].sell_asset_id required | string | — |
orders[].buy_asset_id required | string | null | One of our assets on a swap; null on an exchange (we have no asset row for it). |
orders[].buy_asset_external required | string | null | The exchange's identifier on an exchange (e.g. SOL.SOL); null on a swap. |
orders[].buy_symbol required | string | null | Display ticker of the bought coin on an exchange, so a listing needs no catalogue lookup. Null on a swap. |
orders[].buy_chain required | string | null | Chain of the bought coin on an exchange, as the exchange names it. Null on a swap. |
orders[].destination_address | string | Where the bought coin was paid. Present on EXCHANGE rows ONLY — echoed back so you can confirm it — and absent on a swap, whose output goes to the user's own static wallet address. This payout is irreversible and outside our custody. |
orders[].sell_amount required | string | A monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string. |
orders[].sell_amount_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
orders[].fee required | string | Our fee, taken out of sell_amount — a percentage with a dollar minimum (see GET /v1/pricing), plus network_fee. |
orders[].fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
orders[].network_fee | string | The network part of fee, in the sold asset — a USDT ⇄ TRX trade's own gas on TRON, or an exchange's send. Already inside fee. |
orders[].network_fee_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
orders[].receive_estimate | string | null | What the route estimated. A swap's is in the buy asset's major units; an exchange's is the exchange's decimal string. |
orders[].receive_estimate_base | string | null | Exact base units on a SWAP. Always null on an exchange. |
orders[].receive_minimum | string | null | The floor the route guaranteed. The user is credited what arrives, not this. |
orders[].receive_minimum_base | string | null | Exact base units on a SWAP. Always null on an exchange. |
orders[].received | string | null | What ACTUALLY arrived. Null until the exchange settles. |
orders[].received_base | string | null | Exact base units on a SWAP. Always null on an exchange. |
orders[].providers required | string[] | — |
orders[].status required | string | pending until the sell leg is on-chain, then sent and swapping while the exchange works; completed when it reports the payout. (one of pending, sent, swapping, completed, refunded, failed) |
orders[].tracking_status | string | null | The exchange's own last word on the route, verbatim. Informational. |
orders[].txid | string | null | The on-chain SELL payment that started the order — the handle to show a customer, and the one the exchange's tracker keys on. Null until it is sent. |
orders[].created_at required | string<date-time> | — |
orders[].updated_at required | string<date-time> | — |
{
"orders": [
{
"order_id": "string",
"user_id": "string",
"kind": "swap",
"sell_asset_id": "string",
"buy_asset_id": "string",
"buy_asset_external": "string",
"buy_symbol": "string",
"buy_chain": "string",
"destination_address": "string",
"sell_amount": "1.5",
"sell_amount_base": "1500000000",
"fee": "1.5",
"fee_base": "1500000000",
"network_fee": "1.5",
"network_fee_base": "1500000000",
"receive_estimate": "string",
"receive_estimate_base": "string",
"receive_minimum": "string",
"receive_minimum_base": "string",
"received": "string",
"received_base": "string",
"providers": [
"string"
],
"status": "pending",
"tracking_status": "string",
"txid": "string",
"created_at": "2026-08-31T09:15:00.000Z",
"updated_at": "2026-08-31T09:15:00.000Z"
}
]
}#Example request
curl -X GET 'https://api.cryptoinfra.build/v1/swap/orders' \
-H 'Authorization: Bearer ck_live_your_key'