Orders
The Order endpoints let you place new trades, query existing orders, and cancel orders that have not yet been filled. These endpoints are at the core of every trading strategy.
Order types
TestMax supports four order types, matching the TopStep ProjectX API:
| Value | Type | Description | Required price fields |
|---|---|---|---|
1 | Limit | Executes at the specified price or better | limitPrice |
2 | Market | Executes immediately at the current market price | None |
3 | Stop Limit | Becomes a limit order when the stop price is reached | stopPrice and limitPrice |
4 | Stop | Becomes a market order when the stop price is reached | stopPrice |
Order sides
| Value | Side | Description |
|---|---|---|
0 | Buy | Buy to open a long position or close a short position |
1 | Sell | Sell to open a short position or close a long position |
POST Order/place
Place a new order. This is the primary endpoint for entering and exiting trades.
URL: POST /api/topstep-sim/Order/place
Authentication: Bearer token required.
Request body
{ "accountId": 12345, "contractId": "CON.F.US.ENQ.H25", "type": 2, "side": 0, "size": 1, "limitPrice": null, "stopPrice": null}| Field | Type | Required | Description |
|---|---|---|---|
accountId | number | Yes | Account ID from Account/search or Replay/start |
contractId | string | Yes | Contract ID from Contract/search |
type | number | Yes | Order type: 1 = Limit, 2 = Market, 3 = StopLimit, 4 = Stop |
side | number | Yes | 0 = Buy, 1 = Sell |
size | number | Yes | Number of contracts to trade (integer, 1–1000) |
limitPrice | number or null | Conditional | Required for Limit (1) and StopLimit (3) orders |
stopPrice | number or null | Conditional | Required for Stop (4) and StopLimit (3) orders |
stopLossBracket | object or null | No | Attach a stop-loss bracket: {"ticks": N} — N ticks away from the entry price |
takeProfitBracket | object or null | No | Attach a take-profit bracket: {"ticks": N} — N ticks away from the entry price |
For pending entry orders (limit/stop), brackets are anchored to the order’s entry price, not the current market price — a limit entry far from the market gets its protective orders around the intended fill level.
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "orderId": 1000000042}| Field | Type | Description |
|---|---|---|
orderId | number | Unique integer identifier for the created order. Use it with Order/cancel and to match fills. |
To find out whether the order filled immediately, check Order/searchOpen (still pending) or the filledOrders array returned by Replay/step.
Examples
# Buy 1 contract at market priceresult = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 2, # Market "side": 0, # Buy "size": 1})
if result["success"]: print(f"Order filled: {result['orderId']}")# Buy 1 contract at a limit price of 21500.00result = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 1, # Limit "side": 0, # Buy "size": 1, "limitPrice": 21500.00})
if result["success"]: order_id = result["orderId"] print(f"Limit order placed: {order_id}")# Place a stop-loss: sell 1 contract if price drops to 21480.00result = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 4, # Stop "side": 1, # Sell "size": 1, "stopPrice": 21480.00})
if result["success"]: stop_order_id = result["orderId"] print(f"Stop order placed: {stop_order_id}")# Stop-limit: when price reaches 21520, place a limit buy at 21525result = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 3, # StopLimit "side": 0, # Buy "size": 1, "stopPrice": 21520.00, "limitPrice": 21525.00})Error responses
| Scenario | errorCode | errorMessage |
|---|---|---|
| Size missing, non-integer, ≤ 0, or > 1000 | 7 | "Invalid order size" |
Unknown type or side value | 7 | "Invalid order type or side" |
Invalid or unowned accountId | 1 | "Account not found" |
| Order rejected by the engine (e.g., insufficient margin) | 2 | The rejection reason |
| Challenge no longer active (passed, violated, or ended) | 4 | "Account violation" |
POST Order/search
Search all orders for an account, including filled, pending, and canceled orders.
URL: POST /api/topstep-sim/Order/search
Authentication: Bearer token required.
Request body
{ "accountId": 12345, "startTimestamp": "2025-01-15T00:00:00Z", "endTimestamp": "2025-01-16T00:00:00Z"}| Field | Type | Required | Description |
|---|---|---|---|
accountId | number | Yes | Account ID |
startTimestamp | string | No | Only return orders submitted at or after this time |
endTimestamp | string | No | Only return orders submitted at or before this time |
Response
Returns up to 200 orders, newest first.
{ "success": true, "errorCode": 0, "errorMessage": null, "orders": [ { "id": 1000000042, "accountId": 12345, "contractId": "CON.F.US.ENQ.H25", "symbolId": "NQ", "creationTimestamp": "2025-01-15T14:30:05.000Z", "updateTimestamp": "2025-01-15T14:30:05.000Z", "status": 2, "type": 2, "side": 0, "size": 1, "limitPrice": null, "stopPrice": null, "fillVolume": 1, "filledPrice": 21503.25, "customTag": null } ]}| Field | Type | Description |
|---|---|---|
orders | array | List of all order objects |
orders[].id | number | Order ID |
orders[].accountId | number | Account ID |
orders[].contractId | string | Contract ID |
orders[].symbolId | string | Instrument symbol (e.g., "NQ") |
orders[].creationTimestamp | string | ISO 8601 timestamp of when the order was submitted |
orders[].updateTimestamp | string or null | Fill time, or null if not filled |
orders[].status | number | Order status (1=Partial, 2=Filled, 3=Canceled, 6=Pending) |
orders[].type | number | Order type (1=Limit, 2=Market, 3=StopLimit, 4=Stop) |
orders[].side | number | Order side (0=Buy, 1=Sell) |
orders[].size | number | Requested order size |
orders[].limitPrice | number or null | Limit price, if set |
orders[].stopPrice | number or null | Stop price, if set |
orders[].fillVolume | number | Number of contracts filled so far |
orders[].filledPrice | number or null | Average fill price, if filled |
orders[].customTag | null | Always null (ProjectX schema compatibility) |
Example
result = api("Order/search", {"accountId": ACCOUNT_ID})
if result["success"]: for order in result.get("orders", []): status_map = {1: "Partial", 2: "Filled", 3: "Canceled", 6: "Pending"} side_str = "BUY" if order["side"] == 0 else "SELL" status_str = status_map.get(order["status"], "Unknown") print(f"{side_str} {order['size']} — Status: {status_str}")POST Order/searchOpen
Get only open/pending orders — orders that have not yet been fully filled or canceled.
URL: POST /api/topstep-sim/Order/searchOpen
Authentication: Bearer token required.
Request body
{ "accountId": 12345}| Field | Type | Required | Description |
|---|---|---|---|
accountId | number | Yes | Account ID |
Response
Same field layout as Order/search, but sourced from the live session engine: only pending orders are returned, status is always 1 (open), and fillVolume is 0. If the session has no active engine, an empty orders array is returned.
Example
# Check for unfilled limit ordersresult = api("Order/searchOpen", {"accountId": ACCOUNT_ID})
open_orders = result.get("orders", [])print(f"{len(open_orders)} open orders")
# Cancel all open orders before placing new onesfor order in open_orders: api("Order/cancel", {"accountId": ACCOUNT_ID, "orderId": order["id"]})POST Order/cancel
Cancel a pending order. Only works on orders that have not yet been fully filled.
URL: POST /api/topstep-sim/Order/cancel
Authentication: Bearer token required.
Request body
{ "accountId": 12345, "orderId": 1000000042}| Field | Type | Required | Description |
|---|---|---|---|
accountId | number | Yes | Account ID that owns the order |
orderId | number | Yes | The ID of the order to cancel |
Response
{ "success": true, "errorCode": 0, "errorMessage": null}Example
# Cancel a specific stop-loss orderresult = api("Order/cancel", {"accountId": ACCOUNT_ID, "orderId": stop_order_id})
if result["success"]: print(f"Order {stop_order_id} canceled")else: print(f"Cancel failed: {result['errorMessage']}")Error responses
| Scenario | errorCode | errorMessage |
|---|---|---|
Invalid or unowned accountId | 1 | "Account not found" |
| Unknown order ID, or order already filled/canceled | 2 | "Order not found" |
POST Order/modify
Not supported in the simulator. The endpoint exists for TopStep ProjectX schema compatibility, but every call returns an error:
{ "success": false, "errorCode": 3, "errorMessage": "Order modification not supported in simulator"}To change a pending order, cancel it and place a new one:
# Move a stop-loss order to a new price (trail the stop)api("Order/cancel", {"accountId": ACCOUNT_ID, "orderId": stop_order_id})
result = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 4, # Stop "side": 1, # Sell to close "size": 1, "stopPrice": 21495.00 # New stop level})stop_order_id = result["orderId"]Common patterns
Bracket order (entry + stop-loss + take-profit)
The simplest way is the native bracket parameters on Order/place — the engine manages the protective orders for you:
result = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 2, "side": 0, "size": 1, "stopLossBracket": {"ticks": 80}, # 20 points on NQ (0.25/tick) "takeProfitBracket": {"ticks": 160} # 40 points target (2:1 R:R)})Or manage the legs manually:
# 1. Enter with a market buyentry = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 2, "side": 0, "size": 1})
# 2. Place a stop-lossstop = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 4, # Stop "side": 1, # Sell to close "size": 1, "stopPrice": entry_price - 20 # 20 points risk})
# 3. Place a take-profittp = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 1, # Limit "side": 1, # Sell to close "size": 1, "limitPrice": entry_price + 40 # 40 points target (2:1 R:R)})
stop_id = stop["orderId"]tp_id = tp["orderId"]
# 4. When one fills, cancel the other (in your main loop)positions = get_positions(ACCOUNT_ID)if len(positions) == 0: # Position was closed — cancel remaining order api("Order/cancel", {"accountId": ACCOUNT_ID, "orderId": stop_id}) api("Order/cancel", {"accountId": ACCOUNT_ID, "orderId": tp_id})Cancel all open orders
result = api("Order/searchOpen", {"accountId": ACCOUNT_ID})for order in result.get("orders", []): api("Order/cancel", {"accountId": ACCOUNT_ID, "orderId": order["id"]})print("All open orders canceled")Trailing stop
Order/modify is unsupported, so trail a stop by canceling and re-placing it:
# Inside your main loop, after checking bar data:if position and bar["h"] > highest_high: highest_high = bar["h"] new_stop = highest_high - trail_distance
api("Order/cancel", {"accountId": ACCOUNT_ID, "orderId": stop_order_id}) result = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 4, "side": 1, "size": 1, "stopPrice": new_stop }) stop_order_id = result["orderId"] print(f"[INFO] Trailing stop moved to {new_stop}")