# Swaps

Trade between two coins you hold — the output is credited to the user's balance

4 endpoints. Generated from `docs/openapi.yaml` v1.0.0.

| Endpoint | Summary |
| --- | --- |
| [`POST /v1/swap/quote`](#post-v1-swap-quote) | Price a swap between two coins you hold |
| [`POST /v1/swap/orders`](#post-v1-swap-orders) | Start a swap — sell one coin the user holds, credit them another |
| [`GET /v1/swap/orders/{id}`](#get-v1-swap-orders-id) | Fetch one order |
| [`GET /v1/swap/orders/{id}/detail`](#get-v1-swap-orders-id-detail) | Fetch one order in full — addresses, send leg, origin and timeline |

## POST /v1/swap/quote

**Price a swap between two coins you hold**  ·  _Auth: bearer API key_  ·  `operationId: quoteSwap`

A SWAP trades between assets WE custody, named by OUR `asset_id` on both sides
(GET /v1/assets) — unlike an exchange, which names its buy side by the
exchange's own identifier. There is no destination address to supply and none
to validate: the output lands on the user's own static wallet address and is credited
to their balance.

Indicative only. A quote confers no reservation and holds no balance, and the
route is re-quoted at create time — treat `expires_at` as the moment to ask
again, not as a price lock.

Our fee is charged on the SELL side, taken OUT of `amount` rather than added on
top, and `send_amount` is what reaches the exchange — published so the numbers
add up rather than a fee appearing from nowhere between quote and settlement.

`receive_estimate` is an estimate; `receive_minimum` is the floor this route
guarantees. The user is credited what ACTUALLY ARRIVES, which is why both are
published: the pair is the promise.

Both sides are assets of ours, so unlike an exchange quote every amount here
carries its exact `*_base` twin.

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.

#### Request body

`application/json` · required · schema `SwapQuoteRequest`

| Field | Type | Description |
| --- | --- | --- |
| `sell_asset_id` **required** | `string` | One of OUR assets (GET /v1/assets) — what is sold out of the user's balance. _(minLength `1`; maxLength `64`)_ |
| `buy_asset_id` **required** | `string` | One of OUR assets again — a swap trades between coins we custody, so this is an `asset_id`, NOT the exchange identifier an exchange takes. Must differ from `sell_asset_id`, and both must be enabled. _(minLength `1`; maxLength `64`)_ |
| `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. |

#### Responses

| Status | Body | Meaning |
| --- | --- | --- |
| `200` | `SwapQuote` | An indicative price, with the moment it stops being valid. |
| `400` | `Error` | The request was refused before anything was charged. `error` is one of: validation_error, invalid_amount, invalid_pair (both sides name the same asset, an asset is not enabled, an asset no exchange can route — `swappable: false` on GET /v1/assets, the message names it — the `user_id` is unknown, or the user has no address on the buy asset's chain), no_route (that pair has no liquidity path at this size — nothing is broken, try another amount or asset). |
| `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** — `SwapQuote`

| Field | Type | Description |
| --- | --- | --- |
| `sell_asset_id` **required** | `string` | — |
| `buy_asset_id` **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. 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]+$`)_ |
| `send_amount` **required** | `string` | `sell_amount` minus `fee` — what reaches the exchange, published so the numbers add up. |
| `send_amount_base` **required** | `string` | Exact integer amount in the asset's smallest unit. _(pattern `^[0-9]+$`)_ |
| `receive_estimate` **required** | `string` | Estimated output, in the buy asset's major units. Indicative only. |
| `receive_estimate_base` **required** | `string` | Exact integer amount in the asset's smallest unit. _(pattern `^[0-9]+$`)_ |
| `receive_minimum` **required** | `string` | The floor this route guarantees. The user is credited what ARRIVES, not this — treat it as the worst case you may quote them. |
| `receive_minimum_base` **required** | `string` | Exact integer amount in the asset's smallest unit. _(pattern `^[0-9]+$`)_ |
| `providers` **required** | `string[]` | The route's legs, in order. |
| `estimated_seconds` **required** | `integer | null` | — |
| `expires_at` **required** | `string<date-time>` | ISO-8601. When the quote stops being valid — the moment to re-quote, not a price lock. Same shape as the exchange quote's. |

```json
{
  "sell_asset_id": "usdt_trc20",
  "buy_asset_id": "trx",
  "sell_amount": "1.5",
  "sell_amount_base": "1500000000",
  "fee": "1.5",
  "fee_base": "1500000000",
  "network_fee": "1.5",
  "network_fee_base": "1500000000",
  "send_amount": "1.5",
  "send_amount_base": "1500000000",
  "receive_estimate": "1.5",
  "receive_estimate_base": "1500000000",
  "receive_minimum": "1.5",
  "receive_minimum_base": "1500000000",
  "providers": [
    "string"
  ],
  "estimated_seconds": 1,
  "expires_at": "2026-08-31T04:21:00.000Z"
}
```

#### Example request

```bash
curl -X POST 'https://api.cryptoinfra.build/v1/swap/quote' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "sell_asset_id": "usdt_trc20",
  "buy_asset_id": "trx",
  "amount": "100"
}'
```

## POST /v1/swap/orders

**Start a swap — sell one coin the user holds, credit them another**  ·  _Auth: bearer API key_  ·  `operationId: createSwapOrder`

`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 output is NOT credited yet. It is paid to the user's OWN static wallet address on
the buy asset's chain and credited by the ordinary deposit path once it
confirms — so poll GET /v1/swap/orders/{id} (or watch the user's balance)
rather than reading `201` as completion. The user is credited what ARRIVES, not
`receive_estimate`; `receive_minimum` is the floor the route guaranteed.

`pending` until the sell leg is on-chain, then `sent`/`swapping` while the
exchange works, and `completed` once the output is reported.

The route is re-quoted at execution time rather than carried over from
POST /v1/swap/quote, so an order's numbers are the ones actually executed and
may differ slightly from the quote you showed. Quote first anyway: it is the
cheap way to find out that a pair has no route at this size.

Idempotent on `idempotency_key` — replaying a key returns the ORIGINAL order
instead of starting a second swap; the same key with a DIFFERENT payload is
refused with `409 idempotency_conflict`. Always retry a failed or timed-out
create with the SAME key.

#### Request body

`application/json` · required · schema `SwapOrderRequest`

| Field | Type | Description |
| --- | --- | --- |
| `sell_asset_id` **required** | `string` | One of OUR assets (GET /v1/assets) — what is sold out of the user's balance. _(minLength `1`; maxLength `64`)_ |
| `buy_asset_id` **required** | `string` | One of OUR assets again — a swap trades between coins we custody, so this is an `asset_id`, NOT the exchange identifier an exchange takes. Must differ from `sell_asset_id`, and both must be enabled. _(minLength `1`; maxLength `64`)_ |
| `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. |
| `user_id` **required** | `string` | Your `external_user_id` — whose balance is sold, and whose deposit address on the buy asset's chain receives the output. The user must already have an address on that chain. _(minLength `1`)_ |
| `idempotency_key` | `string` | Replaying a key returns the ORIGINAL order instead of starting a second swap; the same key with a different payload is 409 idempotency_conflict. Use a fresh key per logical swap; always retry with the same one. _(minLength `1`; maxLength `255`)_ |

#### Responses

| Status | Body | Meaning |
| --- | --- | --- |
| `201` | `SwapOrder` | Swap started; the sell leg is queued as a withdrawal. |
| `400` | `Error` | The request was refused before anything was charged. `error` is one of: validation_error, invalid_amount, invalid_pair (both sides name the same asset, an asset is not enabled, an asset no exchange can route — `swappable: false` on GET /v1/assets, the message names it — the `user_id` is unknown, or the user has no address on the buy asset's chain), no_route (that pair has no liquidity path at this size — nothing is broken, try another amount or asset). |
| `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 external_user_id for this tenant. |
| `409` | `Error` | `error` is one of: insufficient_balance (the sell amount would overdraw the user's balance), idempotency_conflict (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>` | — |

```json
{
  "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

```bash
curl -X POST 'https://api.cryptoinfra.build/v1/swap/orders' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "sell_asset_id": "usdt_trc20",
  "buy_asset_id": "trx",
  "amount": "100",
  "user_id": "string"
}'
```

## GET /v1/swap/orders/{id}

**Fetch one order**  ·  _Auth: bearer API key_  ·  `operationId: getSwapOrder`

The authoritative read, for either kind — another merchant's order is simply
`404`. Poll it while `status` is `pending`, `sent` or `swapping`.

On a SWAP, `received` is what actually arrived and was credited to the user's
balance; it stays null until the output confirms, so a `completed` status with
a populated `received` is the signal to tell your customer it is done.

`user_id` is not echoed here — it is returned on create, and by
GET /v1/swap/orders/{id}/detail.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | `string<uuid>` | yes | The `order_id` from create or the list. |

#### Responses

| Status | Body | Meaning |
| --- | --- | --- |
| `200` | `SwapOrder` | The order. |
| `401` | `Error` | Missing, malformed or unknown API key. |
| `404` | `Error` | Unknown order, or one belonging to another merchant — the two are indistinguishable by design. |

**`200` 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>` | — |

```json
{
  "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

```bash
curl -X GET 'https://api.cryptoinfra.build/v1/swap/orders/string' \
  -H 'Authorization: Bearer ck_live_your_key'
```

## GET /v1/swap/orders/{id}/detail

**Fetch one order in full — addresses, send leg, origin and timeline**  ·  _Auth: bearer API key_  ·  `operationId: getSwapOrderDetail`

Everything GET /v1/swap/orders/{id} returns, plus every hop: the exchange's
one-time deposit address, the address the output went to, the on-chain send
with its confirmations, who asked for the order and from where, and the story
in order. Enough to answer "where is my money" from a block explorer without
opening a ticket.

Deliberately narrower than the operator's view of the same order. You see YOUR
facts — your team member, your IP, your addresses, your money — not what the
send cost us in gas or the ledger account codes behind it.

`received_deposit` is LABELLED, never asserted: nothing on-chain ties a swap to
the deposit that credited it, so this is our best match by asset, amount and
time. `confidence` says how far to trust it (`ambiguous` means a second deposit
could equally be this swap's output) and a page should present it as likely,
not as fact. It is always null on an exchange, whose output never reaches us.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | `string<uuid>` | yes | The `order_id` from create or the list. |

#### Responses

| Status | Body | Meaning |
| --- | --- | --- |
| `200` | `SwapOrderDetail` | The order in full. |
| `401` | `Error` | Missing, malformed or unknown API key. |
| `404` | `Error` | Unknown order, or one belonging to another merchant — the two are indistinguishable by design. |

**`200` response body** — `SwapOrderDetail`

| Field | Type | Description |
| --- | --- | --- |
| `order_id` **required** | `string<uuid>` | — |
| `user_id` **required** | `string` | Your `external_user_id`. Always present here, unlike the summary shape. |
| `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` **required** | `string` | Where the output was sent. Always present here for BOTH kinds: on a swap it is the user's own static wallet address on the buy asset's chain (the summary shape omits it there), and on an exchange it is the address you supplied. |
| `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>` | — |
| `send_amount` **required** | `string` | `sell_amount` minus `fee` — what was actually paid to the exchange. |
| `send_amount_base` **required** | `string` | Exact integer amount in the asset's smallest unit. _(pattern `^[0-9]+$`)_ |
| `exchange_address` **required** | `string` | The exchange's one-time deposit address for THIS order — where the sell payment went. The first hop to follow on a block explorer. |
| `swap_id` **required** | `string | null` | The exchange's own id for the route, for a support conversation with them. |
| `sent_at` **required** | `string<date-time> | null` | When the sell payment reached the chain. Null before that. |
| `settled_at` **required** | `string<date-time> | null` | When the exchange reported a terminal state. Null while still moving. |
| `error` **required** | `string | null` | Why the order failed, when it did. |
| `send` **required** | `object | null` | The withdrawal that paid the exchange. Null until it exists. |
| `send.status` **required** | `string` | — |
| `send.txid` **required** | `string | null` | — |
| `send.confirmations` **required** | `integer` | — |
| `send.block_height` **required** | `integer | null` | — |
| `send.error` **required** | `string | null` | — |
| `origin` **required** | `object | null` | Who asked for this order and from where. Null when nothing was recorded. |
| `origin.actor_email` **required** | `string | null` | The dashboard login that asked. Null when an API key did — there is no person. |
| `origin.ip` **required** | `string | null` | — |
| `origin.ip_geo` **required** | `object | null` | Geolocation of `ip`, or null when lookup is off, failed, or the address is private. |
| `origin.user_agent` **required** | `string | null` | — |
| `origin.via` **required** | `string | null` |  _(one of `dashboard_session`, `api_key`, `null`)_ |
| `origin.at` **required** | `string<date-time>` | — |
| `received_deposit` **required** | `object | null` | Our best MATCH for the deposit that credited this swap's output — a label, not an assertion, because nothing on-chain ties the two together. Null on an exchange (its output never reaches us) and until a candidate appears. |
| `received_deposit.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. |
| `received_deposit.amount_base` **required** | `string` | Exact integer amount in the asset's smallest unit. _(pattern `^[0-9]+$`)_ |
| `received_deposit.txid` **required** | `string` | — |
| `received_deposit.status` **required** | `string` | — |
| `received_deposit.credited_at` **required** | `string<date-time> | null` | — |
| `received_deposit.confidence` **required** | `string` | `unconfirmed` is a single plausible match; `ambiguous` means more than one deposit could be this output — show the warning. _(one of `unconfirmed`, `ambiguous`)_ |
| `timeline` **required** | `object[]` | What happened, oldest first — e.g. `Swap requested`, `Fee charged`, `Balance held`, `Payment signed`, `Payment on chain`, `Payment confirmed`, `Output seen on chain`, `Credited to balance`. Human-readable and additive; new steps may appear, so render the list rather than switching on it. |
| `timeline[].at` **required** | `string<date-time>` | — |
| `timeline[].event` **required** | `string` | — |
| `timeline[].detail` **required** | `string | null` | The step's one supporting fact (often a txid). Null when there is none. |

```json
{
  "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",
  "send_amount": "1.5",
  "send_amount_base": "1500000000",
  "exchange_address": "string",
  "swap_id": "string",
  "sent_at": "2026-08-31T09:15:00.000Z",
  "settled_at": "2026-08-31T09:15:00.000Z",
  "error": "string",
  "send": {
    "status": "string",
    "txid": "string",
    "confirmations": 1,
    "block_height": 1,
    "error": "string"
  },
  "origin": {
    "actor_email": "string",
    "ip": "string",
    "ip_geo": {},
    "user_agent": "string",
    "via": "dashboard_session",
    "at": "2026-08-31T09:15:00.000Z"
  },
  "received_deposit": {
    "amount": "1.5",
    "amount_base": "1500000000",
    "txid": "string",
    "status": "string",
    "credited_at": "2026-08-31T09:15:00.000Z",
    "confidence": "unconfirmed"
  },
  "timeline": [
    {
      "at": "2026-08-31T09:15:00.000Z",
      "event": "Payment on chain",
      "detail": "string"
    }
  ]
}
```

#### Example request

```bash
curl -X GET 'https://api.cryptoinfra.build/v1/swap/orders/string/detail' \
  -H 'Authorization: Bearer ck_live_your_key'
```
