Nexus Trades API
Open, trim, close and read your Nexus paper trades from your own bot or script, with your Skylit API key.
The Nexus Trades API lets your own bot open, trim, close and read your Nexus paper trades. Options and stock orders go to your Nexus paper wallet. Futures orders go to your practice account, or to an evaluation or funded account you name. Every order goes through the same checks, the same live prices and the same account rules as an order you place by hand in Nexus.
What it isn't:
- It never sends trades to a broker. Nothing here reaches a real market, even if you've connected a broker to Nexus. The idea is that your bot already traded wherever it trades, and it mirrors that trade into Nexus.
- It isn't a way to pick your own fill. The server prices every fill from the live market when your request arrives. You can't send a time.
- It isn't a market data feed. It tells you what you can trade and what you hold. It doesn't stream quotes, and it doesn't serve futures market data (the market data API and the MCP server don't either).
All tickers, prices, ids and times on this page are made up to show the shape of a call. They aren't real fills and they aren't results.
What you need
- A Skylit API key. Make one on the Developer page, which also has this API's reference under API reference > Nexus Trades.
- A Skylit account that includes Nexus.
- For futures, futures paper trading on your account. Without it, futures calls answer
403 futures_access_denied.
Trades API calls don't spend API credits. They have their own rate limits.
Base URL
https://app.skylit.aiEvery route lives under /api/nexus/v1/.
| Method | Path | What it does |
|---|---|---|
| GET | /api/nexus/v1/trading/capabilities | What this key can trade right now |
| POST | /api/nexus/v1/trades | Open a trade (options, stocks or futures) |
| POST | /api/nexus/v1/trades/{id}/exits | Trim or close an options or stock trade |
| GET | /api/nexus/v1/trades | List your options and stock trades |
| GET | /api/nexus/v1/trades/{id} | Read one trade |
| GET | /api/nexus/v1/trading/accounts/{account} | A futures account: balance, day P/L, rules, positions |
| GET | /api/nexus/v1/trading/accounts/{account}/orders | The futures orders your keys placed in that account |
| POST | /api/nexus/v1/trading/accounts/{account}/close | Close one futures position, or flatten the account |
| GET | /api/nexus/v1/trading/accounts/{account}/orders/working | The futures orders resting on that account |
| GET | /api/nexus/v1/trading/accounts/{account}/orders/{orderId} | One futures order as it stands now |
| POST | /api/nexus/v1/trading/accounts/{account}/orders/{orderId}/cancel | Cancel one resting futures order |
| POST | /api/nexus/v1/trading/accounts/{account}/orders/{orderId}/modify | Move a resting futures order's price |
| POST | /api/nexus/v1/trading/accounts/{account}/orders/cancel | Cancel every resting order on the account, or on one contract |
Each one has its own page, with every field and an example, in the API Reference tab of the Nexus section. The same reference is in the app, under Developer > API reference > Nexus Trades. The OpenAPI spec is at https://www.skylit.ai/docs/nexus-trades-openapi.yaml.
Quick start
Keep your key out of your code
Terminalexport SKYLIT_API_KEY="your key here" export NEXUS="https://app.skylit.ai"Ask what you can trade
Terminalcurl -s "$NEXUS/api/nexus/v1/trading/capabilities" \ -H "Authorization: Bearer $SKYLIT_API_KEY"JSON{ "data": { "assetClasses": [ { "assetClass": "options", "available": true, "orderTypes": ["market"] }, { "assetClass": "stocks", "available": true, "orderTypes": ["market"] }, { "assetClass": "futures", "available": true, "orderTypes": ["market", "limit", "stop"] } ], "orderTypes": ["market"], "testOrders": true, "futures": { "marketOpen": true, "maxOrderQuantity": 100, "orderTypes": ["market", "limit", "stop"], "bracket": ["stopLoss", "takeProfit", "trailingStop"], "modify": ["price"], "contracts": [ { "root": "NQ", "name": "E-mini Nasdaq-100", "symbol": "NQZ26", "tickSize": 0.25, "tickValueCents": 500, "pointValueCents": 2000, "commissionPerSideCents": 214, "livePrice": true } ], "accounts": [ { "id": "3c2b1a90-5e4d-4f3a-8b2c-1d0e9f8a7b6c", "kind": "practice", "name": "Paper", "status": "active", "balanceCents": 5000000, "tradable": true, "allowed": ["read", "open", "reduce", "close", "limit", "stop", "bracket", "cancel", "modify"] } ] }, "limits": { "writesPerMinute": 30, "readsPerMinute": 120 } } }Call this first. Your bot never has to guess which asset classes, contracts and accounts it can trade. When the futures market is closed,
futures.reopensAtsays when it opens again.Rehearse with a test order
Add
"test": true. The order is checked and priced like the real one, then it isn't placed.Terminalcurl -s -X POST "$NEXUS/api/nexus/v1/trades" \ -H "Authorization: Bearer $SKYLIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ticker":"SPY","direction":"call","strike":600,"expiration":"2026-10-16","quantity":2,"test":true}'See Test orders for what a test checks and what it can't.
Open a real paper trade
Terminalcurl -s -X POST "$NEXUS/api/nexus/v1/trades" \ -H "Authorization: Bearer $SKYLIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ticker": "SPY", "direction": "call", "strike": 600, "expiration": "2026-10-16", "quantity": 2, "clientOrderId": "spy-open-0001" }'It answers
201with the trade indataand the fill inmeta.fill. Keepdata.id: you need it to trim or close the trade.Trim it, then close it
Terminal# Trim 1 of 2 curl -s -X POST "$NEXUS/api/nexus/v1/trades/TRADE_ID/exits" \ -H "Authorization: Bearer $SKYLIT_API_KEY" -H "Content-Type: application/json" \ -d '{"quantity":1,"clientOrderId":"spy-trim-0001"}' # Close the rest curl -s -X POST "$NEXUS/api/nexus/v1/trades/TRADE_ID/exits" \ -H "Authorization: Bearer $SKYLIT_API_KEY" -H "Content-Type: application/json" \ -d '{"closeAll":true,"clientOrderId":"spy-close-0001"}'
Futures in one call
curl -s -X POST "$NEXUS/api/nexus/v1/trades" \
-H "Authorization: Bearer $SKYLIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"assetClass":"futures","ticker":"NQ","side":"buy","quantity":1,"clientOrderId":"nq-open-0001"}'No account in the body means your practice account. An order never lands in an evaluation or a funded account unless you name it ("account": "evaluation", "funded" or the account's id).
Futures net per account and contract, so there's no trade id to exit. Close with POST /api/nexus/v1/trading/accounts/{account}/close: send a ticker to close one position, or "closeAll": true to flatten. It closes exactly what you hold and can't flip you.
Your key
Send your Skylit API key on every call:
Authorization: Bearer YOUR_SKYLIT_API_KEYAn X-API-Key: YOUR_SKYLIT_API_KEY header works too. If you send both, Authorization wins.
- A key only ever sees and trades its owner's trades and accounts. An id that belongs to someone else answers
404, the same as an id that doesn't exist. - A key you revoke stops working here within about a minute.
- Treat it like a password: keep it in an environment variable, never in source control or a shared prompt.
Requests and responses
- Send JSON, one object per request, 8 KB at most.
- Unknown fields are refused. A typo like
"quantitty"gets400 bad_requestnaming the field. It's never silently ignored. - Success answers
{"data": ..., "meta": ...}.metais left out when there's nothing to add. - A failure answers
{"error": {"code": "...", "message": "..."}}. Branch oncode. Themessageis written for a person and can change. - Responses are never cached (
Cache-Control: no-store).
Rate limits
Every request counts, refused ones included, so hammering a refused call keeps you throttled.
| Limit | Applies to | Amount |
|---|---|---|
| Writes per key | POSTs from one key | 30 per minute |
| Reads per key | GETs from one key | 120 per minute |
| Writes per member | POSTs across all your keys | 60 per minute |
| Reads per member | GETs across all your keys | 240 per minute |
| Burst | POSTs across all your keys | 5 in any 5 seconds |
| Per network client | Every request, before the key is checked | 300 per minute |
Each answer carries X-RateLimit-Limit and X-RateLimit-Remaining for your per-key limit (writes on a POST, reads on a GET). Over a limit you get 429 rate_limited with a Retry-After header in seconds. Wait that long, then retry. Your key's general Skylit API rate limit applies too, with the same 429.
You can hold at most 50 open options and stock positions opened through the API. The 51st open answers 409 open_position_limit. Futures positions don't count toward it.
Retries and clientOrderId
Networks drop answers. If your bot sends an order and never hears back, it can't tell whether the order filled. Send a clientOrderId you make up, and retry with the same one: you get the first order back and nothing new is placed.
- Format: 1 to 64 characters. Letters, digits, and
_.:-. - Where: the
clientOrderIdfield in the body, or anIdempotency-Keyheader. If you send both they must match. - How long: 24 hours, per member, across all your keys, in one id space for options, stocks and futures.
- A repeat answers
200with the original order andmeta.idempotentReplay: true. (The first answer to an open is201.) - A repeat while the first copy is still running answers
409 request_in_progresswithRetry-After: 1. Wait and send it again. - The same id on a different kind of request answers
409 client_order_id_used. An id that opened a trade can't close one. - Test orders have their own id space, so a bot can go from test mode to live without changing how it makes ids.
- A futures close that finds nothing to close doesn't use up its id.
Time rules
You never send a time. The fill time is the server's clock when your request arrives.
Options and stocks only fill while their market is open (all times ET):
| What | Fills from | Until |
|---|---|---|
| Stocks | 09:30 | 16:00 |
| Most options | 09:30 | 16:00 |
| SPY, QQQ, IWM options that aren't expiring today | 09:30 | 16:15 |
| SPX, SPXW, XSP, RUT, RUTW, VIX, VIXW options that aren't expiring today | 09:30 | 17:00 |
| Any option on its last trading day | 09:30 | 16:00 |
| Early-close days | 09:30 | 13:00 (13:15 for SPY, QQQ, IWM options that aren't expiring) |
Weekends and US market holidays are closed.
Futures trade Sunday 18:00 ET to Friday 17:00 ET, with a break from 17:00 to 18:00 ET each day. Exchange holidays aren't modelled: on one, the session reads as open, there's no live price, and orders answer 503 no_live_price.
Outside those hours an order answers 422 market_closed and nothing fills. It isn't queued for the open.
Price rules
The server prices the fill, for every asset class.
| Asset class | A buy or an open fills at | A sell or a close fills at |
|---|---|---|
| Options | the live ask | the live bid |
| Stocks | the live price | the live price |
| Futures | the last trade, one tick worse for you | the last trade, one tick worse for you |
A fill needs a live price. When the quote is too old you get 503 stale_quote, and with no live price 503 no_live_price. A futures market order that opens or adds needs a trade printed in the last 15 seconds. A close still works on a quiet price. All of these are safe to retry after a moment.
Your own price (options and stocks only). If your bot filled somewhere else and you want Nexus to record that price, send price. It's only taken inside a small window around the live quote:
bid - tolerance <= price <= ask + tolerance| Asset class | Tolerance |
|---|---|
| Options | the larger of 2 ticks or 1% of the mid. A tick is 0.01 under 3.00 and 0.05 from 3.00 up |
| Stocks | the larger of 0.02 or 0.1% of the live price |
Outside the window, ifOutside decides:
ifOutside | Result |
|---|---|
"reject" (the default) | 422 price_outside_market. The message shows the bid and ask it was checked against. Nothing fills |
"market" | The order fills at the server's market price instead |
meta.fill.priceSource says which price was used: "client" (yours) or "market" (the server's).
Futures take no fill price. A market order fills at the price the server sees. A limit or stop order carries the price it rests at, and fills when the market trades through it, the same as an order from the Nexus futures ticket. An API order can never fill better than the same order placed by hand.
A worked example
A small loop on the futures practice account: check what's tradable, rehearse with a test order, place the real one with a clientOrderId, read the position, flatten. Both versions read the key from the environment.
import os
import time
import uuid
import requests
BASE = "https://app.skylit.ai"
KEY = os.environ["SKYLIT_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}
RETRY_STATUSES = {429, 503}
class ApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def call(method, path, body=None, tries=4):
"""One API call. Waits and retries on 429 and 503, raises on anything else."""
for attempt in range(tries):
resp = requests.request(method, BASE + path, headers=HEADERS, json=body, timeout=10)
if resp.ok:
return resp.json()
err = resp.json().get("error", {})
if resp.status_code in RETRY_STATUSES and attempt < tries - 1:
time.sleep(int(resp.headers.get("Retry-After", "2")))
continue
raise ApiError(resp.status_code, err.get("code"), err.get("message"))
def main():
# 1. What can this key trade right now?
caps = call("GET", "/api/nexus/v1/trading/capabilities")["data"]
futures = caps.get("futures")
if not futures or not futures["marketOpen"]:
print("Futures aren't tradable right now.")
return
nq = next((c for c in futures["contracts"] if c["root"] == "NQ"), None)
if not nq or not nq["livePrice"]:
print("No live NQ price right now.")
return
order = {"assetClass": "futures", "ticker": "NQ", "side": "buy", "quantity": 1}
# 2. Rehearse. Same checks and price as the real order, nothing placed.
if caps["testOrders"]:
test = call("POST", "/api/nexus/v1/trades", {**order, "test": True})
print("Test would fill at", test["data"]["fillPrice"])
# 3. The real order. The clientOrderId makes a retry safe.
coid = "nq-" + uuid.uuid4().hex[:16]
try:
placed = call("POST", "/api/nexus/v1/trades", {**order, "clientOrderId": coid})
except requests.RequestException:
# No answer. Send the SAME id again: it can't fill twice.
placed = call("POST", "/api/nexus/v1/trades", {**order, "clientOrderId": coid})
print("Filled", placed["data"]["symbol"], "at", placed["data"]["fillPrice"])
# 4. Read the position.
state = call("GET", "/api/nexus/v1/trading/accounts/practice")["data"]
for pos in state["positions"]:
pnl = pos.get("openPnlCents") # left out when there's no price
shown = "n/a" if pnl is None else f"${pnl / 100:.2f}"
print(pos["side"], pos["quantity"], pos["symbol"], "open P/L", shown)
# 5. Flatten. Safe to send twice: it only closes what's open.
closed = call("POST", "/api/nexus/v1/trading/accounts/practice/close", {"closeAll": True})
for o in closed["data"]["closed"]:
print("Closed", o["quantity"], o["symbol"], "at", o["fillPrice"])
if __name__ == "__main__":
try:
main()
except ApiError as e:
print("Stopped:", e)// Node 18 or newer. Save as a .mjs file so `import` works.
import { randomUUID } from "node:crypto";
const BASE = "https://app.skylit.ai";
const KEY = process.env.SKYLIT_API_KEY;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// One API call. Waits and retries on 429 and 503, throws on anything else.
async function call(method, path, body, tries = 4) {
for (let attempt = 0; attempt < tries; attempt++) {
const resp = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await resp.json();
if (resp.ok) return json;
if ((resp.status === 429 || resp.status === 503) && attempt < tries - 1) {
await sleep(Number(resp.headers.get("Retry-After") ?? 2) * 1000);
continue;
}
const err = json.error ?? {};
throw Object.assign(new Error(`${resp.status} ${err.code}: ${err.message}`), {
status: resp.status,
code: err.code,
});
}
}
async function main() {
// 1. What can this key trade right now?
const caps = (await call("GET", "/api/nexus/v1/trading/capabilities")).data;
const futures = caps.futures;
if (!futures || !futures.marketOpen) {
console.log("Futures aren't tradable right now.");
return;
}
const nq = futures.contracts.find((c) => c.root === "NQ");
if (!nq || !nq.livePrice) {
console.log("No live NQ price right now.");
return;
}
const order = { assetClass: "futures", ticker: "NQ", side: "buy", quantity: 1 };
// 2. Rehearse. Same checks and price as the real order, nothing placed.
if (caps.testOrders) {
const test = await call("POST", "/api/nexus/v1/trades", { ...order, test: true });
console.log("Test would fill at", test.data.fillPrice);
}
// 3. The real order. The clientOrderId makes a retry safe.
const clientOrderId = "nq-" + randomUUID().replaceAll("-", "").slice(0, 16);
let placed;
try {
placed = await call("POST", "/api/nexus/v1/trades", { ...order, clientOrderId });
} catch (e) {
if (e.status) throw e; // the API answered: a real refusal
// No answer. Send the SAME id again: it can't fill twice.
placed = await call("POST", "/api/nexus/v1/trades", { ...order, clientOrderId });
}
console.log("Filled", placed.data.symbol, "at", placed.data.fillPrice);
// 4. Read the position.
const state = (await call("GET", "/api/nexus/v1/trading/accounts/practice")).data;
for (const pos of state.positions) {
// openPnlCents is left out when there's no price
const shown = pos.openPnlCents == null ? "n/a" : `$${(pos.openPnlCents / 100).toFixed(2)}`;
console.log(pos.side, pos.quantity, pos.symbol, "open P/L", shown);
}
// 5. Flatten. Safe to send twice: it only closes what's open.
const closed = await call("POST", "/api/nexus/v1/trading/accounts/practice/close", { closeAll: true });
for (const o of closed.data.closed) {
console.log("Closed", o.quantity, o.symbol, "at", o.fillPrice);
}
}
main().catch((e) => console.log("Stopped:", e.message));Error codes
Every error has the same shape:
{ "error": { "code": "duplicate_position", "message": "You already hold an open position on this contract. Close it first, or add to it in Nexus.", "tradeId": "6e0d7a2b-3c1f-4b7e-a1d5-9f2c8e4b6a01" } }tradeId only comes with duplicate_position.
Key and access
| Status | code | What it means | What your bot should do |
|---|---|---|---|
| 401 | missing_api_key | No key on the request | Send Authorization: Bearer YOUR_SKYLIT_API_KEY |
| 401 | invalid_api_key | The key is wrong or was revoked | Stop. Create a new key |
| 403 | account_suspended | API access is suspended for this account | Stop. Contact support |
| 403 | nexus_access_denied | This Skylit account doesn't include Nexus | Stop. Nothing to retry |
| 403 | futures_access_denied | This account doesn't have futures paper trading yet | Stop sending futures calls |
| 429 | rate_limited | Too many requests | Wait Retry-After seconds, then retry |
| 503 | trades_api_disabled | The trades API is switched off right now | Wait and retry later |
| 503 | api_keys_unavailable | API keys can't be used here right now | Wait and retry later |
| 503 | auth_unavailable | Your key couldn't be checked right now | Retry in a moment |
| 503 | access_check_unavailable | Nexus access couldn't be checked right now | Retry in a moment |
The request itself
| Status | code | What it means | What your bot should do |
|---|---|---|---|
| 400 | bad_request | A field is wrong, unknown, or doesn't belong on this kind of order. The message says which | Fix the request |
| 400 | bad_contract | The ticker, strike, direction, expiration or contract text couldn't be read | Fix the contract |
| 400 | invalid_quantity | quantity is missing or out of range, or you sent quantity and closeAll together | Fix the size |
| 400 | invalid_price | price is out of range, zero or missing on a move | Fix the price, or leave it out |
| 400 | only_market_orders | An options or stock orderType other than market | Leave orderType out |
| 400 | unsupported_asset_class | That asset class isn't tradable here | Check capabilities |
| 400 | test_orders_unavailable | Test orders aren't on right now | Leave test out, or wait |
Options and stocks
| Status | code | What it means | What your bot should do |
|---|---|---|---|
| 404 | trade_not_found | No trade with that id on your account | Check the id. On a "test": true exit: that id isn't a test order, and nothing was closed |
| 409 | duplicate_position | You already hold this contract. tradeId says which trade | Exit that trade first, or skip |
| 409 | trade_closed | The trade is already closed | Stop trying to exit it |
| 409 | open_position_limit | You have 50 open positions opened through the API | Close some first |
| 422 | market_closed | The market is closed for this contract | Wait for the open. Nothing was queued |
| 422 | contract_expired | The contract has expired or stopped trading for the day | Pick a live contract |
| 422 | insufficient_buying_power | Not enough paper buying power | Send a smaller size |
| 422 | price_outside_market | Your price is outside the window. The message shows the bid and ask | Resend with a closer price, with ifOutside: "market", or with no price |
| 503 | no_live_price | No live price for this contract right now | Retry in a moment |
| 503 | stale_quote | The last quote is too old to fill against | Retry in a moment |
Retries
| Status | code | What it means | What your bot should do |
|---|---|---|---|
| 409 | client_order_id_used | This id already did something different in the last 24 hours | Use a new id for a new order |
| 409 | request_in_progress | An order with this id is still being processed | Wait Retry-After (1 second) and send it again |
Futures
| Status | code | What it means | What your bot should do |
|---|---|---|---|
| 404 | account_not_found | No such futures account on your Skylit account, or no running evaluation or funded account | Read capabilities for your account ids |
| 409 | account_locked | The account isn't taking orders. Usually the daily loss limit, and the message says when it reopens | Stop opening. You can still close |
| 409 | account_ended | The account has failed, passed, been closed or archived | Stop trading this account |
| 409 | account_busy | The account is busy with another order | Wait Retry-After (1 second), then retry |
| 422 | account_ambiguous | "evaluation" or "funded" with more than one running. The message lists the ids | Send the id you mean |
| 422 | account_not_tradable | A challenge entry, or a kind of account the API can't trade | Don't trade it through the API. You can read it |
| 422 | contract_not_offered | That product isn't offered | Read capabilities for the list |
| 422 | contract_expired | That contract month has expired | Send the product (like NQ) to get the front month |
| 422 | market_closed | The futures market is closed. The message says when it reopens | Wait. Check futures.reopensAt |
| 422 | reduce_only_would_open | The reduceOnly order would open, add to or flip a position | Re-read the account. You're probably already flat |
| 422 | order_refused | A rule said no: the contract cap, your risk settings, the flat-by-close window, a rolled contract, or a resting order's own checks. The message says which | Read the message. Don't resend unchanged |
| 503 | no_live_price | No fresh price for this contract | Retry in a moment. A close still works when the market has only gone quiet |
| 503 | rules_pending | Your risk rules couldn't be checked just now, so the order wasn't placed | Wait Retry-After (1 second), then retry |
Futures resting orders
| Status | code | What it means | What your bot should do |
|---|---|---|---|
| 400 | unsupported_order_type | A futures orderType that isn't market, limit or stop | Use one of the three |
| 400 | test_modify_unavailable | You tried to move a test order | Cancel it and send a new test order |
| 404 | order_not_found | No order with that id in that account | Check the account in the path. Read orders/working |
| 409 | order_not_working | The order has already filled, was cancelled or was rejected. Nothing was changed | Read the order. If it filled, you have a position |
Our side
| Status | code | What it means | What your bot should do |
|---|---|---|---|
| 500 | internal_error | Something went wrong on our side | With a clientOrderId: retry with the same id. Without one: read your trades or account before you retry |
| 503 | unavailable | Something we needed couldn't be read right now. Nothing was sent | Retry in a moment |
Not available yet
These are refused or missing today. Don't build around them.
| What | What you get today |
|---|---|
| Stop-limit orders | 400 unsupported_order_type on futures |
| Limit, stop or bracket orders for options and stocks | 400 only_market_orders, or 400 bad_request for the bracket fields |
| Changing the size of a resting order | No route. Cancel it and place a new one |
| Adding to an open options or stock position | 409 duplicate_position. Add to it in Nexus |
| Selling options to open | 400 bad_request |
| Trading a challenge entry, or cancelling or moving its orders | 422 account_not_tradable. Reads work |
Futures in GET /api/nexus/v1/trades | Not listed. Use the account read and the order history |
| A push feed of fills or positions | No route. Poll the reads, inside the rate limits |
| Creating, resetting or buying accounts | No route. Do it in Nexus |
| Futures market data (quotes, bars, levels) | Not served here, or by the market data API or MCP server |
| Placing trades from the MCP server | Not yet. Use this API from your own code |
| Sending trades to a broker | Never. This API is for paper trades inside Nexus only |
Last updated