The CrossTrade API, a remotely accessible REST API for NinjaTrader
One token, two brokers: call NinjaTrader 8 and Tradovate over REST, stream fills and P&L over WebSocket, and connect AI agents through MCP, from Python, JavaScript or any language.
The CrossTrade API began as a way to reach NinjaTrader 8 from external code. REST and WebSocket now reach NT8 through the connected Add-On and linked Tradovate accounts directly from the cloud. Those two transports use a CrossTrade Bearer token. The hosted MCP server for AI clients uses a separate OAuth flow and permissions.
If you write your own bots, dashboards or risk tools, this is the map: which surface to use, how to make your first call on each broker, and the limits worth designing around before you go live.
The CrossTrade API product overview covers plans and supported routes. Start here for working requests, then use the endpoint documentation as the contract for your application.
Why an API for NinjaTrader and Tradovate
Most traders who reach for an API already have logic living somewhere else: a Python model, a data pipeline, a scheduler, a spreadsheet that has grown teeth. Rewriting all of it in NinjaScript is slow, and Tradovate's own API leaves you to handle authentication, token renewal and session state yourself.
CrossTrade puts one interface in front of both brokers. You write ordinary HTTP or WebSocket code in Python, JavaScript, Go, C# or anything else, and CrossTrade handles the hop to the platform. On NinjaTrader 8, it runs through the same NT8 Add-On that executes our webhook alerts, the path that handles hundreds of thousands of orders a day. On Tradovate, it runs server-side against the Tradovate login you linked in CrossTrade, so nothing has to be open on your computer.
Pick your surface
| NinjaTrader 8 | Tradovate | |
|---|---|---|
| REST route | https://app.crosstrade.io/v1/api |
https://app.crosstrade.io/v1/api/tv |
| Where it runs | The NT8 Add-On inside your running NinjaTrader | CrossTrade's servers, against your linked Tradovate login |
| WebSocket | Yes: RPC, order and position events, P&L, and live quotes | Yes, with "origin": "tradovate": RPC, order and position events, P&L. No market data |
| Historical bars and quotes | Yes, from your NinjaTrader data feed | No. Bring your own market data |
| AI agents (MCP) | Yes | Yes, through the Tv_* tools |
The rule of thumb is simple. Use REST for request and response work: read an account, place an order, check a result. Use the WebSocket when your code needs to react the moment something changes, instead of polling. Use MCP when the caller is an AI assistant rather than code you wrote.
Build against the right account route
Use REST and WebSocket APIs for NinjaTrader 8 or linked Tradovate accounts. Start with authentication and a read-only account request.
Getting started
The REST and WebSocket APIs are included with CrossTrade Pro. MCP requires Elite.
- Copy your secret key from My Account in the CrossTrade dashboard. It is your bearer token for every request.
- For NinjaTrader 8, install the NT8 Add-On and leave NinjaTrader running with the add-on connected. For Tradovate, link your Tradovate login under My Account → Brokers. That is all Tradovate needs.
- Send every request with an
Authorization: Bearer <token>header.
Your token is the keys to your accounts. Keep it in an environment variable or a secrets manager, never in a TradingView alert, a public repository or an AI chat.
Your first request
A quote from your NinjaTrader data feed, in Python:
import os
import requests
TOKEN = os.environ["CROSSTRADE_TOKEN"]
headers = {"Authorization": f"Bearer {TOKEN}"}
r = requests.get(
"https://app.crosstrade.io/v1/api/market/quote",
headers=headers,
params={"instrument": "ES 12-26"},
)
print(r.status_code, r.json())
NinjaTrader requests use the dated contract name, like ES 12-26 or MNQ 12-26. Bare roots and continuous symbols such as ES1! are not translated on direct API calls, so your code is responsible for rolling to the next contract.
The Tradovate equivalent of "show me where I stand" is the account snapshot, which returns live P&L, positions and working orders for every linked account in one call:
r = requests.get(
"https://app.crosstrade.io/v1/api/tv/accounts/snapshot",
headers=headers,
)
print(r.json())
There is no quote endpoint on the Tradovate side. CrossTrade does not relay Tradovate market data, so a Tradovate bot needs its own price source.
Placing an order on each broker
The two brokers use the same idea with slightly different field names, because each one mirrors its platform's own conventions.
NinjaTrader 8:
order = {
"instrument": "MES 12-26",
"action": "BUY",
"orderType": "MARKET",
"quantity": 1,
"timeInForce": "DAY",
}
r = requests.post(
"https://app.crosstrade.io/v1/api/accounts/Sim101/orders/place",
headers=headers,
json=order,
)
print(r.json())
Tradovate:
order = {
"instrument": "MES1!",
"action": "buy",
"qty": 1,
"orderType": "market",
}
r = requests.post(
"https://app.crosstrade.io/v1/api/tv/accounts/DemoAccount/orders/place",
headers=headers,
json=order,
)
print(r.json())
Start on Sim101 or a Tradovate Demo account. Orders through the API are real the moment they leave your code. On Tradovate, CrossTrade's account controls still apply: an Account Manager lock or a closing-only account blocks an API order exactly as it blocks a webhook, and configured Trade Copier followers receive the copy. Take-profit and stop-loss values sent to Tradovate over REST are absolute prices, not offsets.
Beyond placing orders, both surfaces cover the rest of the order life: cancel, change, cancel-and-replace, close, flatten, reverse, and an order lifecycle endpoint that shows every command and broker report for a single order. That last one is the endpoint to reach for when a result is uncertain, before you think about retrying.
Streaming with the WebSocket
Polling the REST API in a tight loop is the wrong tool for watching fills. The WebSocket at wss://app.crosstrade.io/ws/stream keeps one connection open and pushes events to you as they happen. It authenticates with the same bearer token and serves both brokers on that single connection, told apart by an origin field.
import asyncio, json, os
import websockets
TOKEN = os.environ["CROSSTRADE_TOKEN"]
async def main():
async with websockets.connect(
"wss://app.crosstrade.io/ws/stream",
additional_headers={"Authorization": f"Bearer {TOKEN}"},
) as ws:
# NinjaTrader 8: live quotes plus order, fill and position events
await ws.send(json.dumps({"action": "subscribe", "instruments": ["ES 12-26"]}))
await ws.send(json.dumps({"action": "subscribe", "events": ["orders", "executions", "positions"]}))
# Tradovate: the same events and account P&L, with NinjaTrader closed
await ws.send(json.dumps({"action": "subscribe", "origin": "tradovate",
"events": ["orders", "executions", "positions"]}))
await ws.send(json.dumps({"action": "streamPnl", "origin": "tradovate", "enabled": True}))
async for message in ws:
print(json.loads(message))
asyncio.run(main())
Leaving origin out means NinjaTrader, so clients written before Tradovate streaming keep working unchanged. Tradovate streams run entirely server-side: your computer can be off. On the Tradovate origin you also get execution reports, the broker's acknowledgement of each order command and the only real-time source of a rejection reason.
You can send commands over the same socket too. Any NinjaTrader REST function can be called as an RPC message, and Tv_-prefixed names reach Tradovate:
{"action": "rpc", "id": "tv-1", "api": "Tv_ListPositions", "args": {}}
Two rules matter in practice. Only one WebSocket connection per CrossTrade account can be open at a time, so a second connection closes the first. And after a reconnect or a Tradovate resync frame, pull current positions and orders again rather than expecting missed events to be replayed. The WebSocket API docs list every frame.
Historical bars from NinjaTrader
If NinjaTrader has the data, your code can have it. The bars endpoint returns closed OHLCV bars from your NinjaTrader data feed, which makes it easy to backfill a model or check a signal against the same data your charts use:
r = requests.post(
"https://app.crosstrade.io/v1/api/market/bars",
headers=headers,
json={"instrument": "ES 12-26", "periodType": "minute", "period": 5, "daysBack": 1},
)
bars = r.json()
AI agents and MCP
AI agents get a door of their own. The hosted MCP server at https://app.crosstrade.io/v1/api/mcp lets Claude, ChatGPT, Cursor or any MCP client call CrossTrade through typed tools instead of hand-written HTTP.
MCP has its own sign-in. The agent connects through an OAuth consent screen, and you choose the scope: mcp:read to look, mcp:trade to act. Read scope is the right default. Beyond the trading tools, MCP reaches things REST does not: compiling NinjaScript against your actual install, and running backtests through NinjaTrader's own Strategy Analyzer engine. The MCP docs cover setup and the recommended first calls, and CrossTrade MCP Is Live walks through what an agent can do with it.
Building your own agent instead? The API is written to be read by one. Point your coding assistant at the OpenAPI spec (https://app.crosstrade.io/v1/api/openapi.json) or paste in https://app.crosstrade.io/v1/api/llms-full.txt, which holds the whole API surface in one file. The AI-Assisted Development guide has drop-in rules templates for Claude, Cursor and others.
Rate limits and safe retries
All authenticated API traffic shares one budget per user: 3 requests per second, with a burst of up to 20. REST calls, WebSocket RPC messages and MCP tool calls all draw from it, and frames you receive over the socket never count. A pause of about seven seconds refills the burst. When you hit the limit, the response tells you how long to wait; honor it rather than hammering.
Tradovate can also apply its own temporary penalty when traffic is heavy, which CrossTrade reports separately as broker_rate_limited. Treat it the same way: back off and reduce request pressure.
The more important habit is reconciliation. If an order request times out or returns something ambiguous, do not blindly send it again. Read the order lifecycle or current positions first, then decide. CrossTrade deliberately does not resend an ambiguous Tradovate command, and your integration should follow the same rule.
Where to go next
The API overview indexes every endpoint, with an NT8 and a Tradovate tab on each page. If your strategy already lives in TradingView, you may not need code at all: TradingView Webhook vs REST API vs MCP explains when an alert is the simpler tool. Questions, or something you have built that you want to show off? Bring it to our Discord.
New to CrossTrade? Start your free 7-day trial.
FAQ
Does the CrossTrade API work with Tradovate?
Yes. Tradovate routes live under /v1/api/tv and run server-side against the Tradovate login you link in CrossTrade, so NinjaTrader and the NT8 Add-On are not required. The WebSocket also streams Tradovate order, fill and position events and account P&L with "origin": "tradovate".
Does NinjaTrader need to be running to use the API?
For NinjaTrader 8 requests, yes. They are carried out by the NT8 Add-On inside your running NinjaTrader, which must be open and connected. Tradovate requests do not need NinjaTrader at all.
Which CrossTrade plan includes API access?
The REST and WebSocket APIs are included with Pro. The hosted MCP server for AI agents requires Elite.
Can I get market data through the CrossTrade API?
On NinjaTrader 8, yes: quotes and historical bars come from your NinjaTrader data feed, and live quotes can stream over the WebSocket. On Tradovate, no. CrossTrade does not relay Tradovate market data, so bring your own price source.
What are the CrossTrade API rate limits?
Each user has a shared budget of 3 requests per second with a burst of 20, covering REST, WebSocket RPC and MCP calls together. Frames received over the WebSocket do not count against it.
CrossTrade is execution and automation software, not a broker, financial adviser or trading strategy. Futures trading involves substantial risk. Test in simulation, supervise automated systems, and verify every order through your broker.