Developers/Nexus Trades API (paper trading, Beta)

Nexus Trades API

Open, trim, close and read your Nexus paper trades from your own bot or script, with your Skylit API key.

Beta. Your Skylit API key works here. Create one on the Developer page.

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

Terminal
https://app.skylit.ai

Every route lives under /api/nexus/v1/.

MethodPathWhat it does
GET/api/nexus/v1/trading/capabilitiesWhat this key can trade right now
POST/api/nexus/v1/tradesOpen a trade (options, stocks or futures)
POST/api/nexus/v1/trades/{id}/exitsTrim or close an options or stock trade
GET/api/nexus/v1/tradesList 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}/ordersThe futures orders your keys placed in that account
POST/api/nexus/v1/trading/accounts/{account}/closeClose one futures position, or flatten the account
GET/api/nexus/v1/trading/accounts/{account}/orders/workingThe 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}/cancelCancel one resting futures order
POST/api/nexus/v1/trading/accounts/{account}/orders/{orderId}/modifyMove a resting futures order's price
POST/api/nexus/v1/trading/accounts/{account}/orders/cancelCancel 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

  1. Keep your key out of your code

    Terminal
    export SKYLIT_API_KEY="your key here"
    export NEXUS="https://app.skylit.ai"
  2. Ask what you can trade

    Terminal
    curl -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.reopensAt says when it opens again.

  3. Rehearse with a test order

    Add "test": true. The order is checked and priced like the real one, then it isn't placed.

    Terminal
    curl -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.

  4. Open a real paper trade

    Terminal
    curl -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 201 with the trade in data and the fill in meta.fill. Keep data.id: you need it to trim or close the trade.

  5. 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

Terminal
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:

Text
Authorization: Bearer YOUR_SKYLIT_API_KEY

An 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" gets 400 bad_request naming the field. It's never silently ignored.
  • Success answers {"data": ..., "meta": ...}. meta is left out when there's nothing to add.
  • A failure answers {"error": {"code": "...", "message": "..."}}. Branch on code. The message is 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.

LimitApplies toAmount
Writes per keyPOSTs from one key30 per minute
Reads per keyGETs from one key120 per minute
Writes per memberPOSTs across all your keys60 per minute
Reads per memberGETs across all your keys240 per minute
BurstPOSTs across all your keys5 in any 5 seconds
Per network clientEvery request, before the key is checked300 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 clientOrderId field in the body, or an Idempotency-Key header. 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 200 with the original order and meta.idempotentReplay: true. (The first answer to an open is 201.)
  • A repeat while the first copy is still running answers 409 request_in_progress with Retry-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):

WhatFills fromUntil
Stocks09:3016:00
Most options09:3016:00
SPY, QQQ, IWM options that aren't expiring today09:3016:15
SPX, SPXW, XSP, RUT, RUTW, VIX, VIXW options that aren't expiring today09:3017:00
Any option on its last trading day09:3016:00
Early-close days09:3013: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 classA buy or an open fills atA sell or a close fills at
Optionsthe live askthe live bid
Stocksthe live pricethe live price
Futuresthe last trade, one tick worse for youthe 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:

Text
bid - tolerance  <=  price  <=  ask + tolerance
Asset classTolerance
Optionsthe larger of 2 ticks or 1% of the mid. A tick is 0.01 under 3.00 and 0.05 from 3.00 up
Stocksthe larger of 0.02 or 0.1% of the live price

Outside the window, ifOutside decides:

ifOutsideResult
"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)

Error codes

Every error has the same shape:

JSON
{ "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

StatuscodeWhat it meansWhat your bot should do
401missing_api_keyNo key on the requestSend Authorization: Bearer YOUR_SKYLIT_API_KEY
401invalid_api_keyThe key is wrong or was revokedStop. Create a new key
403account_suspendedAPI access is suspended for this accountStop. Contact support
403nexus_access_deniedThis Skylit account doesn't include NexusStop. Nothing to retry
403futures_access_deniedThis account doesn't have futures paper trading yetStop sending futures calls
429rate_limitedToo many requestsWait Retry-After seconds, then retry
503trades_api_disabledThe trades API is switched off right nowWait and retry later
503api_keys_unavailableAPI keys can't be used here right nowWait and retry later
503auth_unavailableYour key couldn't be checked right nowRetry in a moment
503access_check_unavailableNexus access couldn't be checked right nowRetry in a moment

The request itself

StatuscodeWhat it meansWhat your bot should do
400bad_requestA field is wrong, unknown, or doesn't belong on this kind of order. The message says whichFix the request
400bad_contractThe ticker, strike, direction, expiration or contract text couldn't be readFix the contract
400invalid_quantityquantity is missing or out of range, or you sent quantity and closeAll togetherFix the size
400invalid_priceprice is out of range, zero or missing on a moveFix the price, or leave it out
400only_market_ordersAn options or stock orderType other than marketLeave orderType out
400unsupported_asset_classThat asset class isn't tradable hereCheck capabilities
400test_orders_unavailableTest orders aren't on right nowLeave test out, or wait

Options and stocks

StatuscodeWhat it meansWhat your bot should do
404trade_not_foundNo trade with that id on your accountCheck the id. On a "test": true exit: that id isn't a test order, and nothing was closed
409duplicate_positionYou already hold this contract. tradeId says which tradeExit that trade first, or skip
409trade_closedThe trade is already closedStop trying to exit it
409open_position_limitYou have 50 open positions opened through the APIClose some first
422market_closedThe market is closed for this contractWait for the open. Nothing was queued
422contract_expiredThe contract has expired or stopped trading for the dayPick a live contract
422insufficient_buying_powerNot enough paper buying powerSend a smaller size
422price_outside_marketYour price is outside the window. The message shows the bid and askResend with a closer price, with ifOutside: "market", or with no price
503no_live_priceNo live price for this contract right nowRetry in a moment
503stale_quoteThe last quote is too old to fill againstRetry in a moment

Retries

StatuscodeWhat it meansWhat your bot should do
409client_order_id_usedThis id already did something different in the last 24 hoursUse a new id for a new order
409request_in_progressAn order with this id is still being processedWait Retry-After (1 second) and send it again

Futures

StatuscodeWhat it meansWhat your bot should do
404account_not_foundNo such futures account on your Skylit account, or no running evaluation or funded accountRead capabilities for your account ids
409account_lockedThe account isn't taking orders. Usually the daily loss limit, and the message says when it reopensStop opening. You can still close
409account_endedThe account has failed, passed, been closed or archivedStop trading this account
409account_busyThe account is busy with another orderWait Retry-After (1 second), then retry
422account_ambiguous"evaluation" or "funded" with more than one running. The message lists the idsSend the id you mean
422account_not_tradableA challenge entry, or a kind of account the API can't tradeDon't trade it through the API. You can read it
422contract_not_offeredThat product isn't offeredRead capabilities for the list
422contract_expiredThat contract month has expiredSend the product (like NQ) to get the front month
422market_closedThe futures market is closed. The message says when it reopensWait. Check futures.reopensAt
422reduce_only_would_openThe reduceOnly order would open, add to or flip a positionRe-read the account. You're probably already flat
422order_refusedA 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 whichRead the message. Don't resend unchanged
503no_live_priceNo fresh price for this contractRetry in a moment. A close still works when the market has only gone quiet
503rules_pendingYour risk rules couldn't be checked just now, so the order wasn't placedWait Retry-After (1 second), then retry

Futures resting orders

StatuscodeWhat it meansWhat your bot should do
400unsupported_order_typeA futures orderType that isn't market, limit or stopUse one of the three
400test_modify_unavailableYou tried to move a test orderCancel it and send a new test order
404order_not_foundNo order with that id in that accountCheck the account in the path. Read orders/working
409order_not_workingThe order has already filled, was cancelled or was rejected. Nothing was changedRead the order. If it filled, you have a position

Our side

StatuscodeWhat it meansWhat your bot should do
500internal_errorSomething went wrong on our sideWith a clientOrderId: retry with the same id. Without one: read your trades or account before you retry
503unavailableSomething we needed couldn't be read right now. Nothing was sentRetry in a moment

Not available yet

These are refused or missing today. Don't build around them.

WhatWhat you get today
Stop-limit orders400 unsupported_order_type on futures
Limit, stop or bracket orders for options and stocks400 only_market_orders, or 400 bad_request for the bracket fields
Changing the size of a resting orderNo route. Cancel it and place a new one
Adding to an open options or stock position409 duplicate_position. Add to it in Nexus
Selling options to open400 bad_request
Trading a challenge entry, or cancelling or moving its orders422 account_not_tradable. Reads work
Futures in GET /api/nexus/v1/tradesNot listed. Use the account read and the order history
A push feed of fills or positionsNo route. Poll the reads, inside the rate limits
Creating, resetting or buying accountsNo 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 serverNot yet. Use this API from your own code
Sending trades to a brokerNever. This API is for paper trades inside Nexus only

Last updated

Was this page helpful?