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 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 withBigInt/Decimal; a float silently rounds away the last digits. amountchanges unit with the order class. On themarketorder that opens a position it is a quote amount — margin, fee, spread and buffer reserved from the free balance. On thetakeorder that closes it, it is the base asset size of the position, and nothing is reserved. Step 5 passes the positionsizestraight through.- Closing is a
takeorder, not a market order the other way. Positions are held per direction, so ashortmarket order placed against an openlongopens a second position instead of closing the first. AtakewithtriggerPrice: "0"carries no trigger and fills at market while the call is still open; anamountlarger than the position holds closes it in full. To flatten an account in one call usePOST /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.
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).
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 samePOST /ordersroute — the operation lists what each type requires. - A stop-loss and a take-profit can ride along with the opening order as
stopTriggerPriceandtakeTriggerPrice. GET /accounts/{accountId}/risk-statusis 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.
amounton 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
limitandoffsetand 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 samex-idempotency-keyis 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
-
GET /accounts/{accountId}/risk-status — Get the risk status of the trader account
-
GET /accounts/{accountId}/stats — Get trading stats of the trader account
-
POST /accounts/{accountId}/close-all — Close all positions and orders
-
GET /accounts/risk-status — Get all accounts of current user risk status
-
GET /accounts/with-risk-status — Get all accounts of current user with risk status
-
GET /orders/{accountId}/{asset}/active — Get active orders by ticker
-
GET /orders/{accountId}/{asset}/history — Get orders history by ticker
-
GET /positions/{accountId}/active — Get all active positions
-
GET /positions/{accountId}/portfolio/history — Get all positions history
-
GET /positions/{positionId}/history — Get all events by position
-
PATCH /positions/{positionId}/margin — Change position margin
-
GET /positions/{accountId}/{asset}/history — Get positions history by ticker
-
GET /positions/{accountId}/{asset}/active — Get active positions by ticker
-
GET /positions/{accountId}/{asset}/open-notional — Get current open interest by ticker
-
GET /positions/{accountId}/{asset}/chart-events — Get position events for chart indicators by ticker
-
GET /positions/{accountId}/{asset}/scalping-coefficient — Get scalping coefficient for ticker