---
title: Публичный API Upscale
icon: book-open
apiVersion: d796ac9
---

Программный доступ к торговле в Upscale с личным API-ключом.
Только перечисленные здесь эндпоинты принимают API-ключ — всё остальное требует интерактивной сессии.

<a id="quick-start"></a>

## Быстрый старт

Открытие и закрытие позиции занимает пять вызовов. Всё ниже выполняется на `https://api.upscale.trade` с одним
личным API-ключом в заголовке `Authorization` — см. [Аутентификация](/ru/developers#authentication), чтобы узнать, как его получить.

| # | Вызов | Что это даёт вам |
|---|------|-------------------|
| 1 | `GET /accounts/with-risk-status` | Счета, которыми ключ может торговать, каждый со своей фазой, статусом, балансом и эквити. Возьмите `accountId` у того, на котором будете торговать. |
| 2 | `GET /v2/markets?accountId=…` | Торгуемые рынки с ценой, комиссиями и границами кредитного плеча, по которым проверяется ордер на этом счёте. Возьмите `id` рынка. |
| 3 | `POST /orders` с `type: "market"` | Открывает позицию. `amount` — это резерв в котируемой валюте, который нужно потратить, `leverage` — множитель. |
| 4 | `GET /positions/{accountId}/active` | Открытые позиции. Нужно, потому что ответ ордера не содержит идентификатор позиции. |
| 5 | `POST /orders` с `type: "take"` и `triggerPrice: "0"` | Закрывает эту позицию по рынку. `amount` — это базовый размер для закрытия. |

Три вещи, которые нужно определить перед написанием любого кода:

- **Каждое число — это необработанная строка fp9** — значение, умноженное на 10⁹. 100 USD — это `"100000000000"`, 10x плечо — это
  `"10000000000"`. Читайте и записывайте их с помощью `BigInt` / `Decimal`; число с плавающей запятой незаметно отбрасывает последние цифры.
- **`amount` меняет единицу измерения в зависимости от класса ордера.** Для ордера `market`, который открывает позицию, это **quote**-сумма — маржа, комиссия, спред и буфер, зарезервированные из свободного баланса. Для ордера `take`, который её закрывает, это размер позиции в **базовом активе**, и ничего не резервируется. Шаг 5 передаёт `size` позиции напрямую.
- **Закрытие — это ордер `take`, а не рыночный ордер в обратную сторону.** Позиции ведутся по направлению, поэтому рыночный ордер `short`, размещённый против открытой `long`, открывает вторую позицию вместо закрытия первой. Ордер `take` с   `triggerPrice: "0"` не имеет триггера и исполняется по рынку, пока вызов ещё открыт; `amount`, превышающий удерживаемую позицию, закрывает её полностью. Чтобы закрыть все позиции на счёте одним вызовом, используйте `POST /positions/{accountId}/close-all`.

Оба скрипта ниже полны: задайте `UPSCALE_API_KEY` в окружении и запустите.

<a id="javascript-node-18"></a>

### JavaScript (Node 18+)

Сохраните как `upscale.mjs` и запустите с помощью `node upscale.mjs` — без зависимостей.

```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');
```

<a id="python-3-10"></a>

### Python (3.10+)

Требуется `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')
```

<a id="where-to-go-next"></a>

### Куда двигаться дальше

- Отложенные входы (`limit`, `stop_market`, `stop_limit`) и защитные ордера (`stop`, `trailing_stop`) используют один и тот же
  `POST /orders` маршрут — операция перечисляет, что требуется для каждого типа.
- Стоп-лосс и тейк-профит могут идти вместе с ордером на открытие как `stopTriggerPrice` и `takeTriggerPrice`.
- `GET /accounts/{accountId}/risk-status` — это эндпоинт, который нужно опрашивать для проверки запаса по просадке, пока позиция открыта.


<a id="authentication"></a>

## Аутентификация

Создайте ключ в приложении Upscale (или с помощью `POST /user/api-keys` из аутентифицированной сессии) — необработанный ключ
показывается один раз, в момент создания. Передавайте его как bearer-токен:

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

Ключ наследует разрешения пользователя, которому он принадлежит, и остаётся действительным, пока он не будет удалён, обновлён или не истечёт.

Торговля через API включается отдельно для каждого аккаунта: аккаунт с выключенным `api_trading` отвечает `403`
(`api_trading_not_enabled`) на каждый запрос, сделанный с ключом, а списки аккаунтов полностью его исключают.

<a id="conventions"></a>

## Соглашения

- Суммы, цены, кредитное плечо и множители передаются как **fp9 необработанные целочисленные строки** — значение, масштабированное на 10⁹.
  `"1000000000"` — это 1 USD, `"10000000000"` — это 10x кредитное плечо, `"50000000"` — это 5%.
- fp9 фиксирует масштаб, а не единицу измерения. Сумма в котируемой валюте — это USD; размер базового актива — это единицы торгуемого актива. `amount` в ордере — это
  котируемая сумма в ордерах на увеличение и базовый размер в ордерах на закрытие — каждое поле указывает, какое из двух значений оно содержит.
- Идентификаторы — это uuid v4. К рынкам обращаются по идентификатору при торговле и по тикеру базового актива
  (`BTC`) на эндпоинтах чтения по каждому рынку.
- Списки разбиваются на страницы с помощью `limit` и `offset` и возвращают общее количество рядом со страницей.
- Позиции и ордера привязаны к фазе, в которой аккаунт находится прямо сейчас: то, что относится к завершённой
  фазе, больше не возвращается.

<a id="rate-limits"></a>

## Лимиты запросов

Лимиты считаются по ключу, в фиксированных окнах, отдельно для эндпоинтов чтения (GET) и записи (всё остальное).
По умолчанию: 10 запросов в секунду и 60 в минуту
для чтения, 10 в секунду и 60 в минуту для записи.

Превышение лимита возвращает `429` (`api_key_rate_limit_exceeded`) с заголовком `Retry-After` и телом
содержащим бакет (`read` / `write`), окно, которое сработало, его лимит и `retryAt`.

Ключ подчиняется только своим собственным лимитам. Лимиты для отдельных эндпоинтов, с которыми сталкивается приложение Upscale,
считаются по каждой интерактивной сессии и не применяются к запросам с API-ключом, поэтому ни один эндпоинт здесь не имеет
более строгого собственного лимита.

<a id="errors"></a>

## Ошибки

При сбое возвращается `{ statusCode, message, error }`, где `error` — стабильный машиночитаемый код.
Ветвление выполняйте по этому коду, а не по тексту сообщения.

- `400` — тело, query или path не прошли проверку схемы (`validation_failed`), или торговое предусловие
  было отклонено (`insufficient_balance`, `position_not_available` и коды для отдельных операций ниже).
- `401` — ключ отсутствует, имеет неверный формат, отозван или истёк (`session_expired`).
- `403` — ключ был использован на эндпоинте, который не принимает ключи (`api_key_not_allowed`), счёт принадлежит
  кому-то другому (`account_access_denied`), API-торговля отключена (`api_trading_not_enabled`), счёт больше не
  торгует (`challenge_closed`), не достиг фазы, допускающей торговлю (`account_phase_not_allowed`), или заблокирован
  лимитом управляемого капитала (`funded_limit_trading_locked`), ордер больше не активен
  (`order_not_active`), или рынок приостановлен (`market_paused`), работает только на закрытие (`market_close_only`) или находится вне
  категории, в которой счёт может торговать (`market_category_not_allowed`).
- `404` — нет такого аккаунта (`account_not_found`), ордера (`order_not_found`), позиции (`position_not_found`)
  или рынка (`market_not_found`).
- `409` — запрос с тем же `x-idempotency-key` всё ещё обрабатывается (`idempotency_key_in_flight`),
  аккаунт в данный момент не загружен торговым движком (`account_not_loaded`), или рыночная цена не
  обновлялась достаточно недавно, чтобы торговать по ней (`market_price_stale`).
- `429` — сработал лимит частоты запросов для ключа (`api_key_rate_limit_exceeded`).

Отклонения на уровне ордера возвращаются как `400` с кодом, указывающим проблемную комбинацию полей; каждая торговая
операция перечисляет те, которые она может выдать.


<a id="operations"></a>

## Операции

- [GET /accounts/{accountId}/equity-history — Получить историю эквити счета трейдера](/ru/developers/operations/getaccountequityhistory)

- [GET /accounts/{accountId}/risk-status — Получить статус риска торгового аккаунта](/ru/developers/operations/getaccountriskstatus)

- [GET /accounts/{accountId}/stats — Получить торговую статистику аккаунта трейдера](/ru/developers/operations/getaccounttradingstats)

- [POST /accounts/{accountId}/close-all — Закрыть все позиции и ордера](/ru/developers/operations/closeallpositionsandorders)

- [GET /accounts/events — Получить события торгового аккаунта](/ru/developers/operations/getaccountsevents)

- [GET /accounts/risk-status — Получить статус риска всех аккаунтов текущего пользователя](/ru/developers/operations/getaccountsriskstatus)

- [GET /accounts/with-risk-status — Получить все счета текущего пользователя со статусом риска](/ru/developers/operations/getaccountswithriskstatus)

- [GET /v2/markets — Получить список рынков](/ru/developers/operations/getmarkets)

- [GET /v2/markets/{id} — Получить рынок по id](/ru/developers/operations/getmarket)

- [POST /orders — Создать новый ордер](/ru/developers/operations/createorder)

- [POST /orders/{accountId}/close-all — Закрыть все ордера](/ru/developers/operations/closeallorders)

- [GET /orders/{accountId}/active — Получить все активные ордера](/ru/developers/operations/getactiveorders)

- [GET /orders/{accountId}/{asset}/active — Получить активные ордера по тикеру](/ru/developers/operations/getactiveordersbyticker)

- [GET /orders/{accountId}/{asset}/history — Получить историю ордеров по тикеру](/ru/developers/operations/getordershistorybyticker)

- [PATCH /orders/{orderId} — Изменить триггерную цену ордера](/ru/developers/operations/updateorder)

- [DELETE /orders/{orderId} — Отменить ордер](/ru/developers/operations/cancelorder)

- [GET /positions/{accountId}/active — Получить все активные позиции](/ru/developers/operations/getactivepositions)

- [GET /positions/{accountId}/portfolio/history — Получить всю историю позиций](/ru/developers/operations/getpositionshistory)

- [GET /positions/{positionId}/history — Получить все события по позиции](/ru/developers/operations/getpositionevents)

- [GET /positions/{positionId} — Получить сведения о позиции](/ru/developers/operations/getposition)

- [PATCH /positions/{positionId}/margin — Изменить маржу позиции](/ru/developers/operations/changemargin)

- [POST /positions/{accountId}/close-all — Закрыть все позиции](/ru/developers/operations/closeallpositions)

- [GET /positions/{accountId}/{asset}/history — Получить историю позиций по тикеру](/ru/developers/operations/getpositionshistorybyticker)

- [GET /positions/{accountId}/{asset}/active — Получить активные позиции по тикеру](/ru/developers/operations/getactivepositionsbyticker)

- [GET /positions/{accountId}/{asset}/open-notional — Получить текущий открытый интерес по тикеру](/ru/developers/operations/getopennotionalbyticker)

- [GET /positions/{accountId}/{asset}/chart-events — Получить события позиции для индикаторов графика по тикеру](/ru/developers/operations/getchartevents)

- [GET /positions/{accountId}/{asset}/chart-events/buckets — Получить события позиции для индикаторов графика, сгруппированные по свечам](/ru/developers/operations/getcharteventbuckets)

- [GET /positions/{accountId}/{asset}/scalping-coefficient — Получить коэффициент скальпинга для тикера](/ru/developers/operations/getscalpingcoefficient)