> ## Documentation Index
> Fetch the complete documentation index at: https://developer.uphold.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Trading API introduction

> Estimate prices and place fill-or-kill orders on Uphold: idempotent placement, the order timeline, trades, and how to test every outcome in Sandbox.

The Trading API allows you to estimate the price of trading an asset and to trade it between two accounts of a user, with orders that either trade the whole quantity or nothing.

> [!NOTE]
>
> The Trading API is in **preview**. It is currently available in **Sandbox** only, and not yet in **Production**. In Sandbox, orders are simulated against a fixed order book: no balance moves, no transaction is created and the orders you create are not stored. Reads are served from six sample orders instead.

## Available endpoints

| Endpoint | Description |
| - | - |
| [Estimate price](./prices/estimate-price) | Estimate the price of trading a quantity of an asset. |
| [Create order](./orders/create-order) | Place a fill-or-kill order. |
| [List orders](./orders/list-orders) | List the orders of a user, newest first. |
| [Get order](./orders/get-order) | Retrieve an order by its id. |
| [List order trades](./orders/list-order-trades) | List the trades of an order. |

The Trading API uses the same OAuth 2.0 token endpoint and credentials as the [Core API](../core-api/authentication/request-oauth2-token), with its own scopes: `trading.prices:create`, `trading.orders:create` and `trading.orders:read`.

## Assets and pairs

An order trades one asset for another, and the pair is what says how to read its amounts. The **base** asset is the one bought or sold, and the **quote** asset is the one prices are expressed in. For `BTC` and `USD`, `BTC` is the base asset and `USD` the quote asset.

All amounts are decimal strings and carry no asset. A `quantity` is in the base asset, a `price` is in the quote asset per one unit of the base asset, and a `notional` is a gross traded amount in the quote asset.

Not every pair can be traded. The API does not list the pairs yet: a request for one that cannot be traded is refused with a `409` `asset_pair_not_supported`.

> [!NOTE]
>
> The bounds an order must respect, such as the smallest quantity it accepts or the decimals it allows, are not published yet. An order that breaks one is refused with a `409` whose `details` name the rule and its `limit`, so build against those errors for now. The bounds are published once the venue reports them.

## Price estimates

A price estimate is the volume-weighted price at which a `quantity` of the `base` asset would trade on a `side`, in the `quote` asset, calculated by walking the order book. It is nonbinding: nothing is created, nothing is reserved, and two requests a moment apart can differ.

The response has only the `price` and the `snapshotAt` time of the order book snapshot it was calculated from. It is not the time of the request.

If the order book cannot cover the quantity, the request fails with the depth that is available, so you can retry with a smaller quantity. The estimate does not include fees.

## Orders

An order trades between two accounts of the same user. The `origin` account is debited and the `destination` account is credited with the proceeds. The origin is the base asset account on a `sell` and the quote asset account on a `buy`, and the destination is the other one. The assets of the two accounts are the pair the order trades, so a `sell` whose origin account does not hold the base asset, or a `buy` whose destination does not, is refused with `asset_pair_not_supported`.

### Type and side

* `type` is `market`, which trades at the price the book offers within a protective cap, or `limit`, which trades only at its `price` or better. `price` is required on a limit order and not allowed on a market order.
* `side` is `buy` or `sell` of the base asset.
* The order is sized by its `quantity`, a decimal string in the base asset.

### Fill or kill

Orders are **fill-or-kill**: `timeInForce` is `fok`, and either the whole quantity trades or nothing does. A fill-or-kill order is finished when the create returns, so there is no cancel.

### Retrying safely

The `clientOrderId` is an idempotency key chosen by you, up to 64 characters:

* Replaying a `clientOrderId` with the same payload returns the existing order with a `200`, instead of the `201` of the first request.
* Reusing a `clientOrderId` with a different payload is refused with a `409`.
* After a `5xx` or a timeout, replay the same `clientOrderId` before anything else: the replay returns the order if it was placed and places it if it was not.

> [!WARNING]
>
> Never send a new `clientOrderId` until the replay has answered, or the quantity may be traded twice.

## Order lifecycle

| Status | Description |
| - | - |
| `pending` | The order has filled at the venue and is waiting to be booked. Nothing has executed yet, so retrieve it again until it is `closed`. |
| `closed` | The order is finished. `closed` does not mean it traded: read the `timeline`. |

The contract also defines an `open` status for orders that stay active. A fill-or-kill order is never `open`.

> [!TIP]
>
> New statuses may be added in the future, so treat an unknown status as non-terminal.

### Timeline

The `timeline` lists what happened to the order, oldest first.

| Event | Description |
| - | - |
| `created` | The order was created. |
| `filled` | The whole quantity traded. The order has one trade. |
| `canceled` | Nothing traded because the order could not fill in full. The `reason` is `limit-price-not-met` on a limit order and `insufficient-liquidity` otherwise. |
| `rejected` | The venue refused the order. The `reason` is `venue-closed`, `venue-limit-exceeded` or `venue-rejected`. |
| `failed` | The outcome is unknown until it is reconciled, with the reason `unspecified-error`. Do not assume the order did or did not trade. |

> [!TIP]
>
> More events and reasons may be added in the future.

### Execution and trades

`execution` holds what traded: `quantity`, `notional` and `averagePrice`, which are zeros and `null` when nothing traded. The `notional` is the gross executed amount, before fees. A filled order has one trade, returned by [List order trades](./orders/list-order-trades), and it is a view of the transaction that booked it.

## Testing in Sandbox

Sandbox simulates one pair, `BTC` and `USD`, against a fixed order book with 5 BTC on each side. A seller meets a best bid of `61950.12` and a buyer a best ask of `62010.30`, and the average price gets 0.1% worse for each BTC traded. Every order type and side is simulated.

You need a test user with a BTC account and a USD account. Send a new `clientOrderId` with every order, and a quantity the venue accepts.

Some quantities force an outcome the book cannot produce by itself:

| Quantity (BTC) | You get |
| - | - |
| `0.000101` | The order stays `pending`. |
| `0.000102` | The order is `rejected` with `venue-closed`. |
| `0.000103` | The order is `failed` with `unspecified-error`. |
| `0.000104` | The order is `canceled` with `insufficient-liquidity`. |
| `0.000429` | A `429` `too_many_requests`. |
| `0.000503` | A `503` `service_unavailable`. |

A limit that the book does not reach is `canceled` with `limit-price-not-met`, and a quantity deeper than the book is refused with `insufficient-book-depth`.

> [!WARNING]
>
> Sandbox does not store the orders you create. The order in the response of a create is worked out from the request alone, so it cannot be read back: [Get order](./orders/get-order) and [List order trades](./orders/list-order-trades) reply with `404` for it. To let you try the reads, every user has six sample orders: [List orders](./orders/list-orders) returns them newest first, with a filled limit sell, a pending order, a killed limit order, a filled limit buy, a rejected order and a filled market buy, and you can get each one and read its trades. They are fixed, and they are not the orders you create. The `id` of an order comes from its `clientOrderId`, so sending the same `clientOrderId` again returns the same `id`, but with a `201` instead of a `200`, and reusing it with a different payload is not refused.
