Tradier API Trading Bot in Python: Setup, Orders, and Automation
Build a Tradier API trading bot in Python: sandbox vs production URLs, bearer auth, quotes, option chains, order fields, status polling, and rate limits.
Put this into practice with a watchlist
Build a watchlist, then review each signal’s entry, stop, target, and reasoning. Broker access is optional.
What a Tradier API Trading Bot Needs
A Tradier API trading bot is a script that authenticates with a bearer token, reads quotes from /v1/markets, and posts form-encoded orders to /v1/accounts/{account_id}/orders. You can build a working version in Python with the requests library and no SDK. Every endpoint and field below comes from the official Tradier docs at docs.tradier.com, checked in September 2026.
The most expensive mistake new builders make is treating a 200 OK on order submission as a fill. It is not. The docs say so, and Tradewink's production logs confirmed it. For the broker-agnostic parts of a bot (strategy loop, risk gates, journaling), see how to build a trading bot. This article covers what is specific to Tradier.
Sandbox vs Production: Base URLs and Tokens
Tradier runs two environments with separate tokens. A sandbox token sent to the production host returns a 401, and the reverse also fails (Tradier Account Details page).
| Environment | REST base URL | Data | Purpose |
|---|---|---|---|
| Production | https://api.tradier.com/v1/ | Real-time equities and options | Live account trading |
| Sandbox | https://sandbox.tradier.com/v1/ | Delayed (15 minutes) | Paper trading and integration tests |
| Streaming (HTTP) | https://stream.tradier.com/v1/ | Real-time | Push market and account events |
| Streaming (WebSocket) | wss://ws.tradier.com/v1/ | Real-time | Same, over WebSocket |
Sources: Tradier "Endpoints" and "Market Data" documentation pages.
Three sandbox facts matter for bot design:
- It is a paper account. Simulated fills say nothing about real liquidity or slippage.
- Data is delayed 15 minutes, so a momentum bot tested only there will behave differently in production.
- Greeks and index quotes are not available, so a delta filter cannot be tested there (Tradier Market Data page).
Tradier issues both tokens from the account API settings page. Keep them out of source control; load them from environment variables.
Authentication: Two Headers on Every Request
Every Tradier request needs an Authorization header carrying a bearer token and an Accept header. Order placement and modification add a form-encoded content type (Tradier Trading guide):
Authorization: Bearer YOUR_ACCESS_TOKEN
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Tradier does not accept JSON bodies for orders. In Python requests, pass a dict to data=, not json=. You also need your account ID, a string like 6YA00001; GET /v1/user/profile returns it, so a bot can discover its account at startup. Error codes: 400 is a bad parameter, 401 is a wrong token or wrong environment for that token, 403 is access denied, 404 means the resource does not exist.
Quotes and Option Chains
The two /v1/markets calls a bot uses most:
Quotes. GET /v1/markets/quotes?symbols=AAPL,MSFT returns one quote per symbol with bid, ask, last, and volume. The plural symbols parameter takes a comma-separated list, which is how you stay under the rate limit while watching many tickers.
Option chains. The chain endpoint takes three query parameters (Tradier OpenAPI definition for Get Options Chains):
| Parameter | Required | Meaning |
|---|---|---|
symbol | Yes | Underlying ticker, for example AAPL |
expiration | Yes | Expiration date in YYYY-MM-DD format |
greeks | No | true to include delta, gamma, theta, vega, and IV (default false) |
Get valid expirations first from GET /v1/markets/options/expirations?symbol=AAPL, then request one chain per expiration. Each contract carries its OCC symbol (AAPL210416C00125000 in the docs sample), strike, bid, ask, and open interest. The OCC symbol is what you pass back as option_symbol in an order. Greeks come from ORATS and update hourly in production (Tradier Market Data page). One symbology note: Tradier writes share classes with a slash, so BRK/B, not BRK.B.
Placing Equity and Single-Leg Option Orders
All orders go to POST /v1/accounts/{account_id}/orders. The common fields (Tradier Trading guide):
| Field | Required | Allowed values |
|---|---|---|
class | Yes | equity, option, multileg, combo, oto, oco, otoco |
symbol | Yes | Underlying ticker |
side | Yes | Equity: buy, sell, sell_short, buy_to_cover. Option: buy_to_open, sell_to_open, buy_to_close, sell_to_close |
quantity | Yes | Whole shares for equities, contracts for options |
type | Yes | market, limit, stop, stop_limit |
duration | Yes | day, gtc, pre, post |
price | Conditional | Required for limit and stop_limit |
stop | Conditional | Required for stop and stop_limit |
option_symbol | Options only | Full OCC symbol |
preview | No | true validates without submitting |
tag | No | Your own label for the order |
Two things Alpaca users expect are missing. There is no trailing-stop type, so a bot must ratchet its own stop by modifying the order. And equity quantities must be whole numbers, so fractional-share bots need a different broker.
A single-leg limit order for 10 shares looks like this as form data:
class=equity&symbol=AAPL&side=buy&quantity=10&type=limit&duration=day&price=175.00
A single-leg option order adds option_symbol and uses the open/close side vocabulary:
class=option&symbol=AAPL&option_symbol=AAPL251017C00247500&side=buy_to_open&quantity=1&type=market&duration=day
The docs recommend sending preview=true first. The preview returns result: true plus estimated cost and margin change, and runs the same buying-power checks as a real submission. Bracket orders exist under class=otoco: leg 0 is the entry, legs 1 and 2 are the target and stop, and Tradier cancels whichever exit does not fill. That attaches a stop at entry without a second API call.
A 200 Response Means Accepted, Not Filled
A successful submission returns this shape (Tradier Trading guide):
{ "order": { "id": 20258740, "status": "ok", "partner_id": "..." } }
The docs state that a 200 OK on submission only means the call was well formed and the order was received; it can still be rejected at the brokerage level. The status: "ok" field describes the request, not the trade.
Tradewink learned this in production. Its executor originally logged success=true for Tradier at submit time. During an April 2026 trade audit, two "successful" sells for one position showed in the logs, yet the broker reconcile loop found the position still open. Both sells were accepted and never filled. The fix was to add explicit fill_status tracking and treat only a polled filled status as proof a position changed.
The order lifecycle states, from the Tradier Orders and Trading pages:
| Status | Meaning | Bot action |
|---|---|---|
pending | Received, waiting (for example, for market open) | Keep polling |
open | Live and working | Keep polling, or cancel if stale |
partially_filled | Some quantity executed | Read exec_quantity and remaining_quantity |
filled | Fully executed | Record avg_fill_price; position is real |
expired | Day order lapsed | Position did not change |
canceled | Cancelled | Position did not change |
rejected | Failed brokerage validation | Log the reason; do not retry blindly |
pending_cancel | Cancel requested, not confirmed | Keep polling |
Poll GET /v1/accounts/{account_id}/orders/{order_id}. The response includes avg_fill_price, exec_quantity, and remaining_quantity, so you can compute realized slippage.
Hypothetical example: your bot sends a limit buy for 10 shares at $175.00, gets id: 20258740 back, and polls every 2 seconds:
- Poll 1:
status: open,exec_quantity: 0 - Poll 2:
status: partially_filled,exec_quantity: 6,avg_fill_price: 174.98 - Poll 3:
status: filled,exec_quantity: 10,avg_fill_price: 174.99
Only after poll 3 should the bot write "long 10 AAPL at $174.99" to its position table. If it had recorded the position at submit time and the order had gone to canceled instead, every later exit order would be a naked short attempt.
Streaming as an Alternative to Polling
Tradier's streaming API pushes trades, quotes, and account events instead of making you poll (Tradier Streaming Data page).
The flow: create a short-lived session with POST /v1/markets/events/session (market data) or POST /v1/accounts/events/session (order events), take the returned sessionid, connect to wss://ws.tradier.com/v1/markets/events, and send a JSON subscribe message with symbols, a filter list such as ["trade","quote"], and the sessionid. Session IDs expire quickly, so request one right before connecting. Only one streaming session per user may be open at a time (Tradier WebSocket Market Data Streaming page).
The account event stream is the better answer to the fill problem. Subscribe to account events and react when an order transitions to filled. Keep a polling fallback for reconnects.
Put the setup on a watchlist first
Use the rules in this guide to evaluate a signal’s entry, stop, target, and reasoning before deciding what, if anything, to do.
Rate Limits
Limits are per access token over one-minute windows (Tradier Rate Limiting page):
| Bucket | Covers | Production | Sandbox |
|---|---|---|---|
| Standard | /accounts, /watchlists, /users, /orders (reads) | 120/min | 60/min |
| Market Data | /markets | 120/min | 60/min |
| Trading | Placing, changing, cancelling orders | 60/min | 60/min |
Every rate-limited response carries X-Ratelimit-Allowed, X-Ratelimit-Used, X-Ratelimit-Available, and X-Ratelimit-Expiry headers. Read X-Ratelimit-Available and back off before it hits zero.
Budget math, hypothetical: watching 40 tickers with one batched /quotes call every 5 seconds uses 12 market-data requests per minute. Polling five open orders every 2 seconds uses 150 standard requests per minute, over the 120 cap. That is the concrete reason to stream account events, or to poll GET /v1/accounts/{account_id}/orders once instead of five single-order calls.
Minimal Python Bot with requests
This script previews an order, submits it, and polls until a terminal state. It targets the sandbox; switch the base URL and token to production only after weeks of correct paper behavior.
import os
import time
import requests
BASE = "https://sandbox.tradier.com/v1"
TOKEN = os.environ["TRADIER_SANDBOX_TOKEN"]
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
"Accept": "application/json",
}
TERMINAL = {"filled", "canceled", "rejected", "expired"}
def account_id() -> str:
r = requests.get(f"{BASE}/user/profile", headers=HEADERS, timeout=10)
r.raise_for_status()
acct = r.json()["profile"]["account"]
if isinstance(acct, list):
acct = acct[0]
return acct["account_number"]
def quote(symbol: str) -> dict:
r = requests.get(
f"{BASE}/markets/quotes",
headers=HEADERS,
params={"symbols": symbol},
timeout=10,
)
r.raise_for_status()
return r.json()["quotes"]["quote"]
def place_limit_buy(acct: str, symbol: str, qty: int, price: float, preview: bool) -> dict:
data = {
"class": "equity",
"symbol": symbol,
"side": "buy",
"quantity": qty,
"type": "limit",
"duration": "day",
"price": f"{price:.2f}",
"preview": "true" if preview else "false",
}
r = requests.post(f"{BASE}/accounts/{acct}/orders", headers=HEADERS, data=data, timeout=10)
r.raise_for_status()
return r.json()["order"]
def wait_for_terminal(acct: str, order_id: int, interval: float = 2.0, max_wait: float = 120.0) -> dict:
deadline = time.time() + max_wait
while time.time() < deadline:
r = requests.get(f"{BASE}/accounts/{acct}/orders/{order_id}", headers=HEADERS, timeout=10)
r.raise_for_status()
order = r.json()["order"]
if order["status"] in TERMINAL:
return order
time.sleep(interval)
raise TimeoutError(f"order {order_id} still {order['status']} after {max_wait}s")
if __name__ == "__main__":
acct = account_id()
q = quote("SPY")
limit_price = round(q["bid"], 2)
prev = place_limit_buy(acct, "SPY", 1, limit_price, preview=True)
if not prev.get("result"):
raise SystemExit(f"preview failed: {prev}")
placed = place_limit_buy(acct, "SPY", 1, limit_price, preview=False)
final = wait_for_terminal(acct, placed["id"])
if final["status"] == "filled":
print("filled", final["exec_quantity"], "at", final["avg_fill_price"])
else:
print("not filled:", final["status"])
Notes: data= sends form encoding, which Tradier requires. The profile response is an object for one account and a list for several. The position is only considered open inside the filled branch.
Tradier vs Alpaca vs IBKR for Bot Builders
| Tradier | Alpaca | Interactive Brokers | |
|---|---|---|---|
| API style | REST + WebSocket streaming | REST + WebSocket streaming | TWS API (socket, needs TWS or IB Gateway running) or Client Portal Web API (REST, needs a local Java gateway) |
| Paper environment | Sandbox at sandbox.tradier.com, delayed data, separate token | Free paper account, real-time simulation, separate keys | Paper account for all account holders; live data needs a funded account with subscriptions |
| Order payload | Form-encoded fields | JSON | Contract and Order objects (TWS) or JSON (Web API) |
| Published REST limits | 120/min standard and market data, 60/min trading (production) | Trading API 200/min per account (Alpaca community forum) | Not documented as a single number; not covered here |
| Fractional shares | No (whole numbers for equities) | Yes (qty and notional orders) | Depends on account settings; not covered here |
| Python minimum | Any version that runs requests | Any version that runs the SDK | TWS API requires Python 3.11 or newer (IBKR Campus) |
| Assets via API | US stocks and options | US stocks, ETFs, options, crypto | Global multi-asset |
Sources: Tradier Endpoints, Trading, Market Data, and Rate Limiting pages; Alpaca "About Trading API" page and community forum; IBKR Campus lessons on the TWS API and Client Portal API. Commissions vary by plan: Tradier Lite charges $0.35 per stock trade and $0.35/contract, while Pro ($10/month) is $0 on stocks and equity/ETF options (Tradier pricing, September 2026). Alpaca US stocks are commission-free for retail order flow plus regulatory pass-throughs. Check each pricing page before you go live.
The practical split: Tradier is the simplest REST surface with native options orders and no local gateway. Alpaca has the easiest onboarding and a real-time paper feed, which the Alpaca AI agent tutorial uses. IBKR has the widest coverage and the most setup friction. A fuller framework is in how to choose a broker for algorithmic trading.
Safety Checklist Before Going Live
- Run in the sandbox first and log every order transition. Compare your position table against
GET /v1/accounts/{account_id}/positionsonce per session; mismatches mean you trusted a submit response somewhere. - Never mark a position open or closed until the polled status is
filled. Handlepartially_filledby readingexec_quantity. - Attach exits at entry with
class=otocoso a crash between entry and stop placement does not leave a naked position. - Cap orders per minute in your own code below Tradier's 60/min trading limit so a retry loop cannot spray duplicates.
- Build a kill switch that calls
DELETE /v1/accounts/{account_id}/orders/{order_id}for every open order and refuses new entries. - Keep sandbox and production tokens in differently named environment variables so a typo cannot point a live token at test logic.
- FINRA's pattern-day-trader designation and $25,000 minimum ended June 4, 2026 (SEC approval April 14, 2026; Release 34-105226). They were replaced by an intraday margin standard (25% maintenance throughout the day); the $2,000 margin-account minimum remains, cash accounts still need settled funds, and brokers may phase in through October 20, 2027, so confirm what Tradier enforces on your account.
- Start in paper trading and size small when you switch.
How Tradewink Handles Tradier
Tradewink's open-source Tradier client reads the four X-Ratelimit headers on every response and spaces cancel-all calls to stay under the 60/min trading cap. After a submit it fetches the order by ID rather than trusting the status: "ok" envelope, and a reconcile loop compares broker positions against its own records to catch accepted-but-unfilled orders. Public subscriptions are paper-only; separately approved private beta accounts may submit live broker orders. Public Tradier connections use paper or sandbox accounts; integration details are on the Tradier broker page; a side-by-side with Alpaca is at Tradewink vs Alpaca.
Automated trading through any broker API carries a substantial risk of loss, including from bugs in your own code. This article is educational and is not financial advice.
Frequently Asked Questions
What is the Tradier sandbox base URL?
The sandbox REST base URL is https://sandbox.tradier.com/v1/ and production is https://api.tradier.com/v1/. Each environment has its own access token, and using a sandbox token against the production host returns a 401. Sandbox market data is delayed 15 minutes and Greeks are not available there.
Does a 200 response from the Tradier orders endpoint mean my order filled?
No. Tradier's docs state that a 200 OK on submission only means the request was well formed and the order was received; it can still be rejected by the brokerage. Poll GET /v1/accounts/{account_id}/orders/{order_id} and treat only a filled status as proof the position changed. Tradewink's own audit found accepted sell orders that never filled while the position stayed open.
What fields does a Tradier equity order require?
Every order needs class, symbol, side, quantity, type, and duration, sent as form-encoded fields, not JSON. Limit and stop-limit orders add price; stop and stop-limit orders add stop. Equity sides are buy, sell, sell_short, and buy_to_cover, and quantities must be whole shares.
How do I place an options order with the Tradier API?
Use class=option and add option_symbol with the full OCC symbol, such as AAPL251017C00247500, which you get from GET /v1/markets/options/chains with a symbol and expiration. Option sides are buy_to_open, sell_to_open, buy_to_close, and sell_to_close. Spreads use class=multileg with indexed option_symbol[n], side[n], and quantity[n] fields.
What are the Tradier API rate limits?
Per the Tradier Rate Limiting page, production allows 120 requests per minute for standard account reads and for market data, and 60 per minute for trading calls. The sandbox allows 60 per minute in every bucket. Limits are per access token over one-minute windows, and responses include X-Ratelimit-Available so you can throttle before hitting them.
Should I poll or stream order updates from Tradier?
Streaming is better for anything beyond a few orders. Create a session with POST /v1/accounts/events/session, connect to wss://ws.tradier.com/v1/, and react when an order reaches filled. Polling five orders every 2 seconds would exceed the 120 per minute standard limit, so keep polling as a reconnect fallback rather than the primary path.
Is Tradier or Alpaca better for a Python trading bot?
It depends on what you trade. Tradier offers a plain REST surface with native options order types and a paper sandbox that uses delayed data. Alpaca offers a real-time paper environment, fractional shares, and crypto, with a Trading API limit of about 200 requests per minute per account. Neither choice guarantees a profitable bot, and this is not financial advice.
Read next
Keep learning with a related guide before putting an idea on your watchlist.
How to Build a Trading Bot: Step-by-Step Guide (2026)
Learn how to build a trading bot from scratch — architecture, data feeds, strategy logic, risk management, and broker connectivity. Includes a practical framework for beginner and intermediate algorithmic traders.
Build an AI Trading Agent with Alpaca Python: Paper Only
Build an Alpaca Python paper agent with completed IEX bars, validated model proposals, bounded bracket requests, dry run, and order reconciliation.
How to Choose a Broker for Algorithmic Trading (2026 Guide)
Compare broker APIs, Rule 605 execution quality and Rule 606 routing reports, paper environments and costs using the same order profile.
How to Automate Stock Trading with Alpaca in 2026
Learn Alpaca stock automation with an offline Python request preview, paper-account setup, order states, and failure reconciliation before execution.
Paper Trading App Workflow: Review Stock Signals
Learn to paper trade stock signals by reviewing entry, stop, target, and rationale, then recording and revisiting each decision.
Ready to evaluate a signal?
Start free with a watchlist and inspect the context before you consider a broker connection.
Try AI signals on your watchlist
Send yourself a signal preview, then add tickers to see ranked entries, exits, and risk notes in Tradewink.
Related Signal Types
Tradewink builds explainable market research for self-directed traders. Build a watchlist, inspect signal reasoning and risk context, and paper-track ideas before you decide. Public subscriptions are paper-only; separately approved private beta accounts may submit live broker orders.
How this guide is reviewed
Tradewink reviews educational content against its documented market-data sources, risk controls, and product methodology. See our data sources and evaluation methodology for the evidence and limitations behind the platform.