Short answer: The Tradier API is Tradier Brokerage's REST and streaming interface for US stocks, ETFs and options, and its pricing page, read 6 November 2026, includes API access on every plan, from Lite at $0 a month. The sandbox uses data delayed 15 minutes. The documented trap: an order call can return HTTP 200 even when a downstream system rejects the order, so read its status afterwards.
No Tradier account was available, so nothing here called Tradier with a token. Official pages were read on 6 November 2026; where they disagree, both versions are given. This is not a recommendation of Tradier, and automated trading can lose money. Other brokers are in the stock and broker API guides.
Tradier API pricing: what each plan includes
API access costs nothing extra: Tradier's pricing page, read 6 November 2026, includes it on all three plans.
| Item | Lite | Pro | Pro Plus |
|---|---|---|---|
| Monthly fee | $0 | $10 | $35 |
| Stocks, per trade | $0.35 | $0.00 | $0.00 |
| Equity and ETF options, per contract | $0.35 | $0.00 | $0.00 |
| Index options, per contract | $0.35 | $0.35 | $0.10 |
| Option assignment or exercise | $9.00 | $9.00 | $5.00 |
| API access | Included | Included | Included |
The fee schedule adds pass-through costs, such as clearing at $0.0775 a contract, and a $50 yearly inactivity fee for fewer than 2 trades a year; the account minimum is $0. Futures appear there too but come from Tradier Futures, a separate company.
Hypothetical example. At $0.35 each, 29 stock trades a month cost $10.15 on Lite, more than Pro's $10 fee. Pro Plus costs $25 more than Pro and saves $0.25 per index option contract, breaking even at 100 a month on those commissions alone; each assignment or exercise saves $4 more.
Tradier API documentation: hosts, tokens and personal use
For your own account you need only the token from the API Settings page; without Partner status the API is for personal use only, and partner apps use OAuth 2.0.
- Hosts. Production is
https://api.tradier.com/v1/, the sandboxhttps://sandbox.tradier.com/v1/. A token sent to the other host gets HTTP 401. - Tradier API key. A personal token gives full account access and lasts until you regenerate it, so store it like a password (see API key security).
- Coverage. US equities, ETFs and options; Level 1 quotes only.
- Account types. The profile endpoint lists individual, joint, entity, custodial and IRA classifications, cash or margin, with options levels 1 to 6.
The API User Agreement, a PDF dated 7 October 2019, says Tradier may reject orders placed through the API, counts an order as received only when it actually reaches Tradier, and may revoke an API's authorization at any time without notice.
Tradier sandbox: what paper trading does not show
The sandbox has production's endpoints but delayed data, no market data streaming and lower limits, so it tests code paths, not fills.
| Item | Production | Sandbox |
|---|---|---|
| Market data | Real time; some index data delayed 15 minutes | Delayed 15 minutes |
| Orders | Live equities, ETFs and options | Paper trades filled against delayed data |
| Market data streaming | Available | Not offered |
| Requests a minute: standard, market data, trading | 120, 120, 60 | 60, 60, 60 |
| Futures | Not available | Not available |
Tradier's knowledge base calls the sandbox free and says it mostly mirrors production, and the docs FAQ offers paper accounts to brokerage account holders. No page read says whether someone who cannot open an account can get a sandbox token, or how paper fills, partial fills or slippage are modeled (see paper trading API).
Tradier API rate limit: HTTP 400, not 429
Tradier counts requests per access token per minute and answers an overrun with HTTP 400 and a plain-text body, so code waiting for a 429 never sees one.
- Scope. Each 1-minute interval starts with the first request. Limits are per token, so programs sharing one personal token share one budget. Placing orders counts as trading.
- Headers.
X-Ratelimit-Allowed,X-Ratelimit-Used,X-Ratelimit-AvailableandX-Ratelimit-Expiry, the last in epoch milliseconds. - Over the limit. The rate limit page, citing Tradier's live testing, gives HTTP 400 with
Quota Violation: Expires <epoch-ms>and a non-standardplain/textcontent type. Match that substring before parsing JSON.
The official FAQs disagree on raising limits: the documentation FAQ says most likely not, the knowledge base FAQ offers custom arrangements for specialized needs.
Why an HTTP 200 does not mean Tradier accepted the order
Tradier's error page says the place-order call returns 200 for errors thrown by downstream systems such as risk management and market centers, so read the order's status afterwards.
- Status names disagree. The single-order schema lists
pending,open,partially_filled,filled,expired,canceled,rejectedandpending_cancel; the order list's filter spellscancelledand omitspending_cancel. Accept both spellings (see order state machine). - Undocumented errors. The error page says such an order carries an
errorsproperty that the documented order fields leave out, so log it whole. - No client order ID. The only label you can attach is an optional
tag, and no page read says tags are unique or block duplicates. Order lists show tags only withincludeTags=true, cover the current day's session and return 25 orders unless you raiselimit(see client order ID). - One item is not a list. One order comes back as an object, several as an array.
A preview=true request runs all validation, including buying power, and returns cost and commission, but it cannot see a later downstream rejection. The error table still describes TooSmallEquityForDayTrading as a pattern day trader violation below $25,000 of equity. FINRA Regulatory Notice 26-10 replaced those rules, including the $25,000 minimum, with intraday margin standards effective 4 June 2026 and lets firms phase them in until 20 October 2027; the pages read do not say when Tradier's checks change.
Tradier API Python: order code tested offline
This sample refuses a tag already used today, reads the status before trusting a 200, treats a lost or unreadable reply as unknown instead of resending, and detects the 400 rate limit reply. It ran on Python 3.11.3 on 6 November 2026. Tradier's overview links community SDKs it does not maintain; review one as you would your own code.
import json, re, time
from decimal import Decimal, InvalidOperation
class Lost(Exception): pass # reply lost: the order may exist
class Unknown(Exception): pass # reconcile; never resend blindly
class RateLimited(Exception): pass
def price_text(p, cap=Decimal("100000")):
try:
d = Decimal(str(p))
except InvalidOperation:
d = Decimal("NaN")
if d.is_finite() and 0 < d < cap and d == d.quantize(Decimal("0.0001")):
return format(d.normalize(), "f") # never "inf" or "1E-5" on the wire
raise ValueError(f"bad price {repr(p)}")
class Tradier:
def __init__(self, token, account, send, host="sandbox.tradier.com"):
if not (token or "").strip(): # fail before any network call
raise ValueError("missing token")
self.base, self.send = f"https://{host}/v1", send
self.orders = f"/accounts/{account}/orders"
self.headers = {"Authorization": f"Bearer {token.strip()}", "Accept": "application/json"}
def call(self, method, path, data=None):
try: # send(): GET data as query, POST data as form
status, body = self.send(method, self.base + path, data, self.headers)
except OSError as err:
raise Lost(type(err).__name__) from None
if status >= 500:
raise Lost(f"HTTP {status}")
if status == 401: # bad token or wrong host
raise PermissionError("HTTP 401: token or host")
if status == 429 or status == 400 and "Quota Violation" in body: # 400 seen, 429 listed
m = re.search(r"Expires (\d+)", body)
wait = int(m.group(1)) / 1000 - time.time() if m else 60
raise RateLimited(f"wait {max(wait, 0):.0f} s")
try:
payload = json.loads(body)
except ValueError:
payload = None
if status == 200 and isinstance(payload, dict):
return payload
if status == 200: # received, reply unreadable
raise Lost("HTTP 200, unreadable reply")
raise RuntimeError(f"HTTP {status}: {body[:60]}")
def orders_tagged(self, tag): # covers today's session only
reply = self.call("GET", self.orders, {"includeTags": "true", "limit": "1000"})
box = reply.get("orders")
rows = box.get("order") if isinstance(box, dict) else None
rows = rows if isinstance(rows, list) else [rows] if isinstance(rows, dict) else []
return [r for r in rows if isinstance(r, dict) and r.get("id") and r.get("tag") == tag]
def place(self, symbol, side, qty, tag, price=None):
if not re.fullmatch(r"[A-Za-z0-9-]{1,40}", tag or ""):
raise ValueError(f"bad tag {repr(tag)}")
if isinstance(qty, bool) or not isinstance(qty, int) or not 1 <= qty <= 10_000_000:
raise ValueError(f"bad quantity {repr(qty)}")
data = {"class": "equity", "symbol": symbol, "side": side, "quantity": str(qty),
"type": "market" if price is None else "limit", "duration": "day", "tag": tag}
if price is not None:
data["price"] = price_text(price)
try: # refuse a reused tag
used = self.orders_tagged(tag)
except Lost as err:
raise RuntimeError(f"nothing sent: {err}") from None
if used:
raise ValueError(f"tag {tag} already used today")
try:
order = self.call("POST", self.orders, data).get("order")
order_id = order.get("id") if isinstance(order, dict) else None
except Lost as err:
try:
hits = self.orders_tagged(tag)
except Exception as err2:
raise Unknown(f"{err}, lookup failed: {type(err2).__name__}") from None
if len(hits) == 1:
order_id = hits[0]["id"]
else:
raise Unknown(f"{err}, {len(hits)} orders tagged {tag}") from None
if not order_id:
raise Unknown("HTTP 200 without an order id")
try: # a 200 is not acceptance
order = self.call("GET", f"{self.orders}/{order_id}").get("order")
except Exception as err:
raise Unknown(f"order {order_id} sent, status not read: {err}") from None
status = order.get("status") if isinstance(order, dict) else None
return f"order {order_id}: {status or 'unknown'}"
# offline tests: synthetic replies only
def fake(script):
def send(*request):
send.calls += 1
step = script.pop(0)
if isinstance(step, Exception):
raise step
return step
send.calls = 0
return send
clean = (200, '{"orders": {"order": {"id": 100}}}')
ok = (200, '{"order": {"id": 101, "status": "ok"}}')
cases = {
"200, then rejected": [clean, ok, (200, '{"order": {"id": 101, "status": "rejected"}}')],
"rate limited": [clean, (400, f"Quota Violation: Expires {int(time.time() * 1000) + 12000}")],
"timeout, one match": [clean, TimeoutError(), (200, '{"orders": {"order": {"id": 102, "tag": "t-1"}}}'),
(200, '{"order": {"id": 102, "status": "open"}}')],
"HTTP 502, two matches": [clean, (502, "x"), (200, '{"orders": {"order": [{"id": 105}, '
'{"id": "", "tag": "t-1"}, {"id": 103, "tag": "t-1"}, {"id": 104, "tag": "t-1"}]}}')],
}
for name, script in cases.items():
try:
result = Tradier("TOKEN", "VA000000", fake(script)).place("AAPL", "buy", 1, "t-1")
except (Unknown, RateLimited, PermissionError, RuntimeError) as err:
result = f"{type(err).__name__}: {err}"
print(f"{name:23} {result}")
send = fake([])
for price, tag in [(float("1e400"), "t-2"), ("1e-400", "t-2"), (None, "")]:
try:
Tradier("TOKEN", "VA000000", send).place("AAPL", "buy", 1, tag, price)
except ValueError as err:
print(f"{'bad input':23} {err}")
try:
Tradier(" ", "VA000000", send)
except ValueError as err:
print(f"{'empty token':23} {err}; requests sent: {send.calls}")
Output:
200, then rejected order 101: rejected
rate limited RateLimited: wait 12 s
timeout, one match order 102: open
HTTP 502, two matches Unknown: HTTP 502, 2 orders tagged t-1
bad input bad price inf
bad input bad price '1e-400'
bad input bad tag ''
empty token missing token; requests sent: 0
Not run: no call reached Tradier, so error bodies, a real rejected order and sandbox fills are untested. In requests 2.31.0, timeout and connection errors subclass OSError; check any other HTTP library. The tag format and check (one more request per order), price cap and four decimals are this sample's choices; the quantity range is from Tradier's error table. With no match or several, the code stops for a person, because a fresh order may not be listed yet (see position reconciliation).
Who can open a Tradier account, and rules for Turkey
API access follows account access, which Tradier's country list governs.
Tradier's knowledge base list of permitted and blocked countries, read 6 November 2026, says Tradier Brokerage cannot open accounts for residents of Turkey, the United Kingdom, Canada or Australia, among others; citizens of the last three living in a permitted country can apply. Rules depend on your country, and account types and API access vary; no broker is recommended or ranked here.
For readers in Turkey, the Capital Markets Board (SPK) said on 23 February 2023 that residents may trade leveraged products such as forex only through institutions it has authorized, that offering them otherwise is a crime, and that it acts to block websites offering them (Law No. 6362 Articles 99(4) and 109(2)). Options, futures and leverage carry high risk. For integration work in general, see Fincode's broker API integration page; naming Tradier here does not mean Fincode integrates it or has built on it.
Frequently asked questions
Is the Tradier API free?
Tradier's pricing page, read 6 November 2026, includes API access on Lite ($0 a month), Pro ($10) and Pro Plus ($35). Commissions vary by plan; pass-through fees can apply.
Does Tradier have a sandbox for paper trading?
Yes, at sandbox.tradier.com/v1 with its own token, using data delayed 15 minutes and without market data streaming.
Do Tradier API tokens expire?
Personal tokens last until you regenerate them; partner OAuth access tokens last 24 hours.
Can I use the Tradier API outside the United States?
Only where Tradier can open your account, because API access follows account access. Its country list, read 6 November 2026, excludes residents of Turkey, the United Kingdom, Canada and Australia, among others.
Sources
- Tradier Docs: Overview (environments, tokens, order status note, scope), read 6 November 2026 (opens in a new tab)
- Tradier Docs: Rate Limits (categories, headers, HTTP 400 Quota Violation) (opens in a new tab)
- Tradier Docs: Error Responses (HTTP 200 on downstream rejection, error codes) (opens in a new tab)
- Tradier Docs: FAQ (sandbox delay, streaming, personal use, token expiry, API cost) (opens in a new tab)
- Tradier Docs: Preview Order (validation including buying power, cost and commission) (opens in a new tab)
- Tradier Docs: Get Single Order (status values) (opens in a new tab)
- Tradier Docs: Get Account Orders (includeTags, status filter, session scope) (opens in a new tab)
- Tradier Docs: Get User Profile (account classifications, option level) (opens in a new tab)
- Tradier: Pricing and Plans with Fee Schedule, read 6 November 2026 (opens in a new tab)
- Tradier Brokerage: Application Program Interface User Agreement (PDF dated 7 October 2019) (opens in a new tab)
- Tradier Knowledge Base: Permitted and Blocked Countries (opens in a new tab)
- Tradier Knowledge Base: Tradier API FAQ (countries, cost, limits, sandbox) (opens in a new tab)
- FINRA Regulatory Notice 26-10: intraday margin standards replace the day trading rules (effective 4 June 2026, phase-in to 20 October 2027) (opens in a new tab)
- SPK (Capital Markets Board): Notice on unauthorized leveraged transactions, 23 February 2023 (Turkish) (opens in a new tab)
- Capital Markets Law No. 6362, consolidated text, Articles 99 and 109 (Turkish) (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.