---
title: Upscale Public API
icon: book-open
apiVersion: d796ac9
---

Programmatic access to Upscale trading with a personal API key.
Only the endpoints listed here accept an API key — everything else requires an interactive session.

## Quick start

Opening and closing a position takes five calls. Everything below runs against `https://api.upscale.trade` with a single
personal API key in the `Authorization` header — see [Authentication](/developers#authentication) for how to get one.

| # | Call | What it gives you |
|---|------|-------------------|
| 1 | `GET /accounts/with-risk-status` | The accounts the key may trade, each with its phase, status, balance and equity. Take `accountId` from the one to trade on. |
| 2 | `GET /v2/markets?accountId=…` | The tradable markets with the price, fees and leverage bounds an order on that account is checked against. Take `id` of the market. |
| 3 | `POST /orders` with `type: "market"` | Opens the position. `amount` is the quote reserve to spend, `leverage` the multiplier. |
| 4 | `GET /positions/{accountId}/active` | The open positions. Needed because the order response carries no position identifier. |
| 5 | `POST /orders` with `type: "take"` and `triggerPrice: "0"` | Closes that position at market. `amount` is the base size to close. |

Three things to settle before writing any code:

- **Every number is an fp9 raw string** — the value scaled by 10⁹. 100 USD is `"100000000000"`, 10x leverage is
  `"10000000000"`. Read and write them with `BigInt` / `Decimal`; a float silently rounds away the last digits.
- **`amount` changes unit with the order class.** On the `market` order that opens a position it is a **quote**
  amount — margin, fee, spread and buffer reserved from the free balance. On the `take` order that closes it,
  it is the **base asset** size of the position, and nothing is reserved. Step 5 passes the position `size` straight through.
- **Closing is a `take` order, not a market order the other way.** Positions are held per direction, so a `short`
  market order placed against an open `long` opens a second position instead of closing the first. A `take` with
  `triggerPrice: "0"` carries no trigger and fills at market while the call is still open; an `amount` larger than
  the position holds closes it in full. To flatten an account in one call use `POST /positions/{accountId}/close-all`.

Both scripts below are complete: set `UPSCALE_API_KEY` in the environment and run.

### JavaScript (Node 18+)

Save as `upscale.mjs` and run with `node upscale.mjs` — no dependencies.

```js
import { randomUUID } from 'node:crypto';

const BASE_URL = 'https://api.upscale.trade';
const API_KEY = process.env.UPSCALE_API_KEY;

// Human decimal -> fp9 raw string: 100 -> "100000000000", "0.5" -> "500000000".
const toFp9 = (value) => {
  const [whole, fraction = ''] = String(value).split('.');
  return BigInt(whole + fraction.padEnd(9, '0').slice(0, 9)).toString();
};

// fp9 raw string -> human decimal string. Stays on BigInt: Number loses the low digits above ~9M USD.
const fromFp9 = (raw) => {
  const negative = raw.startsWith('-');
  const digits = (negative ? raw.slice(1) : raw).padStart(10, '0');
  return `${negative ? '-' : ''}${digits.slice(0, -9)}.${digits.slice(-9)}`;
};

const call = async (method, path, body) => {
  const response = await fetch(`${BASE_URL}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      // One key per logical request; a retry of that request must send the same key again.
      ...(body ? { 'Content-Type': 'application/json', 'x-idempotency-key': randomUUID() } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const payload = response.status === 204 ? null : await response.json();
  if (!response.ok) {
    // `error` is the stable machine-readable code — branch on it, never on `message`.
    throw new Error(`${method} ${path} -> ${response.status} ${payload?.error}: ${payload?.message}`);
  }
  return payload;
};

// 1. Accounts. Authenticated with an API key the list holds only accounts that have api_trading enabled.
const accounts = await call('GET', '/accounts/with-risk-status');
const account = accounts.find((item) => item.status === 'active' && item.apiTrading && item.type === 'demo');
if (!account) {
  throw new Error('No active demo account with API trading enabled');
}
console.log(`account ${account.accountId}, equity ${fromFp9(account.riskStatus.currentEquity)}`);

// 2. Markets, priced by the shard that hosts this account — the state the order will be filled against.
const markets = await call('GET', `/v2/markets?accountId=${account.accountId}`);
const market = markets.find((item) => item.config.baseAsset === 'BTC');
if (!market) {
  throw new Error('BTC is not available on this account');
}
console.log(`${market.config.ticker} at ${fromFp9(market.state.indexPrice)}`);

// 3. Open a long: 100 USD of reserve at 10x, filled at market.
const order = await call('POST', '/orders', {
  accountId: account.accountId,
  marketId: market.id,
  type: 'market',
  direction: 'long',
  amount: toFp9(100),
  leverage: toFp9(10),
  expectedAmount: '0', // no slippage check; pass the base size you expect to enforce one
});
console.log(`order ${order.id} ${order.status}`);

// 4. Read the position back — the order response does not carry its identifier.
const positions = await call('GET', `/positions/${account.accountId}/active`);
const position = positions.find((item) => item.market === market.id && item.direction === 'long');
if (!position) {
  throw new Error('Position not found — the order may have been deferred or rejected at execution');
}
console.log(`position ${position.idx}, size ${fromFp9(position.size)} ${market.config.baseAsset}`);

// 5. Close it at market.
await call('POST', '/orders', {
  accountId: account.accountId,
  marketId: market.id,
  type: 'take',
  direction: position.direction, // a close order runs in the same direction as its position
  positionId: position.idx,
  amount: position.size, // base units
  triggerPrice: '0', // no trigger: fill now
});
console.log('closed');
```

### Python (3.10+)

Needs `requests` (`pip install requests`).

```python
import os
import uuid
from decimal import Decimal

import requests

BASE_URL = 'https://api.upscale.trade'
API_KEY = os.environ['UPSCALE_API_KEY']
FP9 = Decimal(10) ** 9


def to_fp9(value) -> str:
    """Human decimal -> fp9 raw string: 100 -> "100000000000"."""
    return str(int(Decimal(str(value)) * FP9))


def from_fp9(raw: str) -> Decimal:
    """fp9 raw string -> Decimal. Never float — it drops the low digits."""
    return Decimal(raw) / FP9


def call(method: str, path: str, body: dict | None = None):
    headers = {'Authorization': f'Bearer {API_KEY}'}
    if body is not None:
        # One key per logical request; a retry of that request must send the same key again.
        headers['x-idempotency-key'] = str(uuid.uuid4())
    response = requests.request(method, f'{BASE_URL}{path}', json=body, headers=headers, timeout=30)
    payload = None if response.status_code == 204 else response.json()
    if not response.ok:
        # `error` is the stable machine-readable code — branch on it, never on `message`.
        raise RuntimeError(
            f'{method} {path} -> {response.status_code} {payload.get("error")}: {payload.get("message")}'
        )
    return payload


# 1. Accounts. Authenticated with an API key the list holds only accounts that have api_trading enabled.
accounts = call('GET', '/accounts/with-risk-status')
account = next((item for item in accounts if item['status'] == 'active' and item['apiTrading'] and item['type'] == 'demo'), None)
if account is None:
    raise SystemExit('No active demo account with API trading enabled')
print(f"account {account['accountId']}, equity {from_fp9(account['riskStatus']['currentEquity'])}")

# 2. Markets, priced by the shard that hosts this account — the state the order will be filled against.
markets = call('GET', f"/v2/markets?accountId={account['accountId']}")
market = next((item for item in markets if item['config']['baseAsset'] == 'BTC'), None)
if market is None:
    raise SystemExit('BTC is not available on this account')
print(f"{market['config']['ticker']} at {from_fp9(market['state']['indexPrice'])}")

# 3. Open a long: 100 USD of reserve at 10x, filled at market.
order = call('POST', '/orders', {
    'accountId': account['accountId'],
    'marketId': market['id'],
    'type': 'market',
    'direction': 'long',
    'amount': to_fp9(100),
    'leverage': to_fp9(10),
    'expectedAmount': '0',  # no slippage check; pass the base size you expect to enforce one
})
print(f"order {order['id']} {order['status']}")

# 4. Read the position back — the order response does not carry its identifier.
positions = call('GET', f"/positions/{account['accountId']}/active")
position = next((item for item in positions if item['market'] == market['id'] and item['direction'] == 'long'), None)
if position is None:
    raise SystemExit('Position not found — the order may have been deferred or rejected at execution')
print(f"position {position['idx']}, size {from_fp9(position['size'])} {market['config']['baseAsset']}")

# 5. Close it at market.
call('POST', '/orders', {
    'accountId': account['accountId'],
    'marketId': market['id'],
    'type': 'take',
    'direction': position['direction'],  # a close order runs in the same direction as its position
    'positionId': position['idx'],
    'amount': position['size'],  # base units
    'triggerPrice': '0',  # no trigger: fill now
})
print('closed')
```

### Where to go next

- Deferred entries (`limit`, `stop_market`, `stop_limit`) and protective orders (`stop`, `trailing_stop`) take the same
  `POST /orders` route — the operation lists what each type requires.
- A stop-loss and a take-profit can ride along with the opening order as `stopTriggerPrice` and `takeTriggerPrice`.
- `GET /accounts/{accountId}/risk-status` is the endpoint to poll for drawdown headroom while a position is open.


## Authentication

Create a key in the Upscale app (or with `POST /user/api-keys` from an authenticated session) — the raw key
is shown once, at creation time. Pass it as a bearer token:

```
Authorization: Bearer usk_<your key>
```

A key inherits the permissions of the user it belongs to and stays valid until it is deleted, refreshed or expires.

Trading over the API is enabled per account: an account with `api_trading` turned off answers `403`
(`api_trading_not_enabled`) to every request made with a key, and account lists leave it out entirely.

## Conventions

- Amounts, prices, leverage and multipliers travel as **fp9 raw integer strings** — the value scaled by 10⁹.
  `"1000000000"` is 1 USD, `"10000000000"` is 10x leverage, `"50000000"` is 5%.
- fp9 fixes the scale, not the unit. A quote amount is USD; a base size is units of the traded asset. `amount` on an order is
  quote on increase orders and base on close orders — every field says which of the two it carries.
- Identifiers are uuid v4. Markets are addressed by identifier when trading and by base asset ticker
  (`BTC`) on the per-market read endpoints.
- Lists page through `limit` and `offset` and return the total count next to the page.
- Positions and orders are scoped to the phase the account is in right now: what belongs to a finished
  phase is no longer returned.

## Rate limits

Limits are counted per key, in fixed windows, separately for read (GET) and write (everything else) endpoints.
Defaults are 10 requests per second and 60 per minute
for reads, 10 per second and 60 per minute for writes.

Exceeding a limit returns `429` (`api_key_rate_limit_exceeded`) with a `Retry-After` header and a body
carrying the bucket (`read` / `write`), the window that tripped, its limit and `retryAt`.

The key's own limits are the only ones a key is subject to. The per-endpoint limits the Upscale app runs into
are counted per interactive session and are not applied to API-key requests, so no endpoint here carries a
tighter limit of its own.

## Errors

A failure answers with `{ statusCode, message, error }`, where `error` is a stable machine-readable code.
Branch on that code, not on the message text.

- `400` — the body, query or path failed schema validation (`validation_failed`), or a trading precondition
  was rejected (`insufficient_balance`, `position_not_available`, and the per-operation codes below).
- `401` — the key is missing, malformed, revoked or expired (`session_expired`).
- `403` — the key was used on an endpoint that does not accept keys (`api_key_not_allowed`), the account belongs
  to someone else (`account_access_denied`), API trading is off (`api_trading_not_enabled`), the account is no
  longer trading (`challenge_closed`), has not reached a tradable phase (`account_phase_not_allowed`) or is locked
  by the managed capital limit (`funded_limit_trading_locked`), the order is no longer active
  (`order_not_active`), or the market is paused (`market_paused`), close-only (`market_close_only`) or outside
  the category the account may trade (`market_category_not_allowed`).
- `404` — no such account (`account_not_found`), order (`order_not_found`), position (`position_not_found`)
  or market (`market_not_found`).
- `409` — a request with the same `x-idempotency-key` is still being processed (`idempotency_key_in_flight`),
  the account is not currently loaded by the trading engine (`account_not_loaded`), or the market price has not
  been updated recently enough to trade on (`market_price_stale`).
- `429` — the rate limit of the key tripped (`api_key_rate_limit_exceeded`).

Order-level rejections come back as `400` with a code naming the field combination at fault; each trading
operation lists the ones it can produce.


## Operations

- [GET /accounts/{accountId}/equity-history — Get the equity history of the trader account](/developers/operations/getaccountequityhistory)

- [GET /accounts/{accountId}/risk-status — Get the risk status of the trader account](/developers/operations/getaccountriskstatus)

- [GET /accounts/{accountId}/stats — Get trading stats of the trader account](/developers/operations/getaccounttradingstats)

- [POST /accounts/{accountId}/close-all — Close all positions and orders](/developers/operations/closeallpositionsandorders)

- [GET /accounts/events — Get the events of the trader account](/developers/operations/getaccountsevents)

- [GET /accounts/risk-status — Get all accounts of current user risk status](/developers/operations/getaccountsriskstatus)

- [GET /accounts/with-risk-status — Get all accounts of current user with risk status](/developers/operations/getaccountswithriskstatus)

- [GET /v2/markets — Get markets list](/developers/operations/getmarkets)

- [GET /v2/markets/{id} — Get market by id](/developers/operations/getmarket)

- [POST /orders — Create new order](/developers/operations/createorder)

- [POST /orders/{accountId}/close-all — Close all orders](/developers/operations/closeallorders)

- [GET /orders/{accountId}/active — Get all active orders](/developers/operations/getactiveorders)

- [GET /orders/{accountId}/{asset}/active — Get active orders by ticker](/developers/operations/getactiveordersbyticker)

- [GET /orders/{accountId}/{asset}/history — Get orders history by ticker](/developers/operations/getordershistorybyticker)

- [PATCH /orders/{orderId} — Change order trigger price](/developers/operations/updateorder)

- [DELETE /orders/{orderId} — Cancel order](/developers/operations/cancelorder)

- [GET /positions/{accountId}/active — Get all active positions](/developers/operations/getactivepositions)

- [GET /positions/{accountId}/portfolio/history — Get all positions history](/developers/operations/getpositionshistory)

- [GET /positions/{positionId}/history — Get all events by position](/developers/operations/getpositionevents)

- [GET /positions/{positionId} — Get position details](/developers/operations/getposition)

- [PATCH /positions/{positionId}/margin — Change position margin](/developers/operations/changemargin)

- [POST /positions/{accountId}/close-all — Close all positions](/developers/operations/closeallpositions)

- [GET /positions/{accountId}/{asset}/history — Get positions history by ticker](/developers/operations/getpositionshistorybyticker)

- [GET /positions/{accountId}/{asset}/active — Get active positions by ticker](/developers/operations/getactivepositionsbyticker)

- [GET /positions/{accountId}/{asset}/open-notional — Get current open interest by ticker](/developers/operations/getopennotionalbyticker)

- [GET /positions/{accountId}/{asset}/chart-events — Get position events for chart indicators by ticker](/developers/operations/getchartevents)

- [GET /positions/{accountId}/{asset}/chart-events/buckets — Get position events for chart indicators grouped by candles](/developers/operations/getcharteventbuckets)

- [GET /positions/{accountId}/{asset}/scalping-coefficient — Get scalping coefficient for ticker](/developers/operations/getscalpingcoefficient)