Developers/Nexus Trades API (paper trading, Beta)

Live stream

Get your own fills, orders, trades and account changes pushed to your bot the moment they happen, instead of polling.

Your bot doesn't have to keep asking "did it fill yet?" anymore. Open one stream and Nexus pushes your own changes the moment they happen: a fill, an order resting or cancelled, a trade opened or closed, and your wallet or futures account right after. It's Server-Sent Events: one long HTTP response that sends an event per change.

Terminal
curl -N "https://app.skylit.ai/api/nexus/v1/trading/stream" \
  -H "Authorization: Bearer $SKYLIT_API_KEY"
  • Same key, same checks. It checks your key, a suspended account and Nexus access exactly like every other route. Opening it counts as one read. Events and heartbeats don't count.
  • Only yours. Nobody else's trades ever show up, and nobody else ever sees yours. Your agent account's events come to you and no one else. Test orders never show up: they aren't trades.
  • One book or both. Add ?account=main for your own trades and accounts, or ?account=agent for your bot's. Leave it out to get both. Anything else answers 400 bad_request. Every event says which book it's on.
  • Futures events only come if your account has futures. The connected event tells you with "futures": true or false.
  • It never streams market data. Quotes aren't in it.

What comes down the stream

EventWhendata
connectedOnce, first{"account":"all","resumed":false,"futures":true,"heartbeatSeconds":15}
resyncFirst thing on a new connection, and any time the server lost track{"reason":"connected"}. Re-read your state with the REST routes
tradeAn options or stock trade was opened, added to, trimmed, closed or expiredchange, book, at, fill, trade
orderA resting order changedchange, assetClass, book, order
walletYour options and stocks cash or buying power movedbook, wallet
accountA futures account changed (balance, P/L, positions, working orders)book, account
heartbeatEvery 15 seconds{"at":"2026-10-12T14:31:00Z"}
closedWe ended the stream. Fix the reason before you reconnect{"reason":"invalid_api_key"}
reconnectThe server is restarting{"reason":"server_restart"}. Reconnect right away

resync's reason is for your logs (connected, reconnected, lagged, bus or error). Your bot does the same thing for all of them. closed says one of invalid_api_key (the key was revoked or is no longer valid), account_suspended, nexus_access_denied or trades_api_disabled (the API is switched off for now, so wait a while before you try again).

The objects inside are the same ones the REST routes return, field for field:

  • trade is what GET /api/nexus/v1/trades/{id} returns, as the trade stands when the event goes out. fill is this one fill: {"quantity", "price"}, plus id when the server knows the fill's own id (it's left out today, so don't rely on it). change is opened, added, trimmed, closed or expired.
  • order is what the order read returns: GET .../wallets/{book}/orders/{orderId} for options and stocks, GET .../accounts/{account}/orders/{orderId} for futures. Orders you placed in Nexus show up too, without "source": "api".
  • wallet is GET /api/nexus/v1/trading/wallets/{book}. account is GET /api/nexus/v1/trading/accounts/{account}.

change on an order tells you what just happened to it:

changeMeans
workingIt's resting and can fill
heldA futures stop or target waiting for its entry to fill
modifiedYou (or Nexus) moved its price or size
triggeredA stop went off. It now works as an order and can't be cancelled
partial_fillPart of it filled. The rest still works
filledDone. fillPrice is what you got
cancelled, rejected, expiredDone, no fill. statusReason says why

Here's a fill as it comes over the wire:

Text
id: 5c1f0e9a2b7d-1042
event: order
data: {"change":"filled","assetClass":"options","book":"main","order":{"id":"8f0c...","status":"filled","fillPrice":1.98,"filledQuantity":2,"tradeId":"41aa...", ...}}

id: 5c1f0e9a2b7d-1043
event: trade
data: {"change":"opened","book":"main","at":"2026-10-12T14:31:07Z","fill":{"quantity":2,"price":1.98},"trade":{"id":"41aa...","ticker":"SPY", ...}}

Dropped connections

Every data event has an id. When you reconnect, send the last one you got as the Last-Event-ID header (or ?lastEventId=). If the server you land on still has what you missed (it keeps your last 128 events for 2 minutes after you drop), you get them and carry on, and connected says "resumed": true. If it doesn't, the first event after connected is resync.

So write your bot one way: on resync, read your open trades, working orders, wallet and futures accounts with the REST routes, then keep going. That covers both cases.

Limits and timing

  • Streams. A key can hold 3 streams at once and your account 6 across all your keys. Past that you get 429 stream_limit_reached with Retry-After. One stream is all a bot needs.
  • If it goes quiet. A heartbeat comes every 15 seconds. If you hear nothing for 45, the connection's dead: drop it and reconnect with your Last-Event-ID.
  • A revoked key's stream ends within about 2 minutes, with closed.
  • A browser's EventSource can't send your key in a header. That's fine: your key shouldn't be in a browser anyway. Run the stream from your bot.
  • capabilities has a stream block: available, route, events, accounts, resume, heartbeatSeconds, maxStreamsPerKey, replayWindowSeconds (120) and replayEvents (128). If available is false, poll the reads instead.
StatuscodeWhat it meansWhat your bot should do
429stream_limit_reachedToo many live streams open for this key (3) or your account (6)Close one, or wait Retry-After seconds
503stream_unavailableThe live stream isn't on, or the server is restartingTry again shortly, or poll the reads

Examples

import json
import os
import time

import requests

NEXUS = "https://app.skylit.ai"
KEY = os.environ["SKYLIT_API_KEY"]


def events(last_id=None):
    """Yield (id, event, data) from the stream until it ends."""
    headers = {"Authorization": f"Bearer {KEY}", "Accept": "text/event-stream"}
    if last_id:
        headers["Last-Event-ID"] = last_id
    # The read timeout is 45 s: three missed heartbeats means a dead stream.
    with requests.get(f"{NEXUS}/api/nexus/v1/trading/stream",
                      headers=headers, stream=True, timeout=(10, 45)) as r:
        if r.status_code != 200:
            raise RuntimeError(r.json()["error"])
        eid, event, data = None, None, []
        for line in r.iter_lines(decode_unicode=True):
            if line == "":
                if event:
                    yield eid, event, json.loads("".join(data)) if data else None
                eid, event, data = None, None, []
            elif line.startswith("id: "):
                eid = line[4:]
            elif line.startswith("event: "):
                event = line[7:]
            elif line.startswith("data: "):
                data.append(line[6:])


def resync():
    # Re-read what you hold: GET /api/nexus/v1/trades?status=open,
    # GET /api/nexus/v1/trading/wallets/main/orders/working, and so on.
    pass


last_id = None
while True:
    try:
        for eid, event, data in events(last_id):
            if eid:
                last_id = eid
            if event == "resync":
                resync()
            elif event == "trade":
                print(data["change"], data["trade"]["ticker"], data["fill"])
            elif event == "order":
                print(data["assetClass"], data["change"], data["order"]["id"])
            elif event == "closed":
                raise SystemExit(f"stream closed: {data['reason']}")
    except (requests.ConnectionError, requests.Timeout):
        pass  # dropped or went quiet: reconnect below
    time.sleep(2)

Last updated

Was this page helpful?