Skip to content
Back to App

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:

ValueTypeDescriptionRequired price fields
1LimitExecutes at the specified price or betterlimitPrice
2MarketExecutes immediately at the current market priceNone
3Stop LimitBecomes a limit order when the stop price is reachedstopPrice and limitPrice
4StopBecomes a market order when the stop price is reachedstopPrice

Order sides

ValueSideDescription
0BuyBuy to open a long position or close a short position
1SellSell 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
}
FieldTypeRequiredDescription
accountIdnumberYesAccount ID from Account/search or Replay/start
contractIdstringYesContract ID from Contract/search
typenumberYesOrder type: 1 = Limit, 2 = Market, 3 = StopLimit, 4 = Stop
sidenumberYes0 = Buy, 1 = Sell
sizenumberYesNumber of contracts to trade (integer, 1–1000)
limitPricenumber or nullConditionalRequired for Limit (1) and StopLimit (3) orders
stopPricenumber or nullConditionalRequired for Stop (4) and StopLimit (3) orders
stopLossBracketobject or nullNoAttach a stop-loss bracket: {"ticks": N} — N ticks away from the entry price
takeProfitBracketobject or nullNoAttach 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
}
FieldTypeDescription
orderIdnumberUnique 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 price
result = 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']}")

Error responses

ScenarioerrorCodeerrorMessage
Size missing, non-integer, ≤ 0, or > 10007"Invalid order size"
Unknown type or side value7"Invalid order type or side"
Invalid or unowned accountId1"Account not found"
Order rejected by the engine (e.g., insufficient margin)2The 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"
}
FieldTypeRequiredDescription
accountIdnumberYesAccount ID
startTimestampstringNoOnly return orders submitted at or after this time
endTimestampstringNoOnly 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
}
]
}
FieldTypeDescription
ordersarrayList of all order objects
orders[].idnumberOrder ID
orders[].accountIdnumberAccount ID
orders[].contractIdstringContract ID
orders[].symbolIdstringInstrument symbol (e.g., "NQ")
orders[].creationTimestampstringISO 8601 timestamp of when the order was submitted
orders[].updateTimestampstring or nullFill time, or null if not filled
orders[].statusnumberOrder status (1=Partial, 2=Filled, 3=Canceled, 6=Pending)
orders[].typenumberOrder type (1=Limit, 2=Market, 3=StopLimit, 4=Stop)
orders[].sidenumberOrder side (0=Buy, 1=Sell)
orders[].sizenumberRequested order size
orders[].limitPricenumber or nullLimit price, if set
orders[].stopPricenumber or nullStop price, if set
orders[].fillVolumenumberNumber of contracts filled so far
orders[].filledPricenumber or nullAverage fill price, if filled
orders[].customTagnullAlways 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
}
FieldTypeRequiredDescription
accountIdnumberYesAccount 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 orders
result = 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 ones
for 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
}
FieldTypeRequiredDescription
accountIdnumberYesAccount ID that owns the order
orderIdnumberYesThe ID of the order to cancel

Response

{
"success": true,
"errorCode": 0,
"errorMessage": null
}

Example

# Cancel a specific stop-loss order
result = 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

ScenarioerrorCodeerrorMessage
Invalid or unowned accountId1"Account not found"
Unknown order ID, or order already filled/canceled2"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 buy
entry = api("Order/place", {
"accountId": ACCOUNT_ID,
"contractId": CONTRACT_ID,
"type": 2, "side": 0, "size": 1
})
# 2. Place a stop-loss
stop = 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-profit
tp = 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}")