Short answer: A stock trading API lets software read quotes and accounts and send orders to a broker. As read on 5 November 2026, Alpaca, E*TRADE, Interactive Brokers, Schwab, tastytrade, TradeStation, Tradier and Webull (alphabetical, not a ranking) each document one, while Robinhood's published REST API covers crypto and its stock access for software is an MCP connection for AI agents. They differ where programs break: Tradier answers rate limits with HTTP 400, tastytrade does not deduplicate retried orders, and E*TRADE requires a preview before an order.
This list compares what an engineer must design around, from each broker's official documents as read on 5 November 2026. No account or key was used. Calling each sandbox without a token returned an authentication error, which proves only that the host answers, and the Python below runs on synthetic replies.
No broker is recommended here. Automated trading can lose money. Architecture is in broker API integration and trading bot development, and data licensing in stock market data API.
Brokers with API trading: docs, login and test environment
Most publish self-service documentation, but access still depends on an account, and each login model implies a different routine.
| Broker | Login | Test environment | Access per its docs |
|---|---|---|---|
| Alpaca | Key headers (opens in a new tab) | paper-api.alpaca.markets, separate keys | Paper-only account open to anyone globally |
| E*TRADE | OAuth request-token flow (opens in a new tab), HMAC-SHA1 signature | apisb.etrade.com | Portal developer.etrade.com refused access; docs read on the sandbox host |
| Interactive Brokers | Web API (opens in a new tab): Client Portal Gateway for individual accounts, OAuth for others; more in IBKR Web API vs TWS API | Paper account (opens in a new tab), logged in with its own Paper username | Open, funded account (opens in a new tab), IBKR Pro for individuals (opens in a new tab), paper included; some login methods need approval |
| Schwab | OAuth 2 authorization code (opens in a new tab) | Not confirmed by Schwab; the community wrapper's README says none | Schwab brokerage account, one app, approval |
| tastytrade | OAuth2 (opens in a new tab), User-Agent of the form product/version required | api.cert.tastyworks.com, separate credentials | Registered OAuth client |
| TradeStation | OAuth2 authorization code (opens in a new tab) | sim-api.tradestation.com/v3 | Funded account, key requested by email (opens in a new tab) |
| Tradier | Bearer token (opens in a new tab) | sandbox.tradier.com/v1 | Brokerage account; API at no extra charge; personal use only without Partner status |
| Webull | HMAC-SHA1 signature (opens in a new tab) with app key | api.sandbox.webull.com | Application (opens in a new tab), reviewed in 1 to 2 business days; shared test accounts need none |
TradeStation recommends v3, and warns it is not liable for applications that let users switch between SIM and live.
Tradier API, TradeStation API, tastytrade: what test environments hide
Each sandbox misleads in its own way, so learn what it fakes before trusting a passing test.
| Broker | Behavior |
|---|---|
| Alpaca | Partial fills at random 10% of the time; no market impact, queue position, price improvement or dividends |
| E*TRADE | Fill behavior not described on the pages read; the authorization pages show a sandbox URL with an empty host |
| tastytrade | Fills at quote mid, some tickers scripted; /market-data returns HTTP 502; trades and positions reset every 24 hours |
| TradeStation | Fake accounts and money, with instant simulated fills |
| Tradier | Market data delayed 15 minutes, paper fills against it; a token works only in its own environment |
| Webull | Generally lower limits than production, for example 30 order calls per 60 seconds against 600 |
Instant fills never exercise your partial-fill path, and a 24-hour reset wipes state you may be testing. For a plan that covers these gaps, see paper trading API.
Rate limits: the numbers and what going over looks like
Do not assume HTTP 429, because Tradier reports limits with HTTP 400 and a plain-text body.
| Broker | Limit | Over the limit |
|---|---|---|
| Alpaca | 200 requests a minute per account (support page dated December 2022) | 429 |
| E*TRADE | Not stated on the pages read | Order API error list: 400, code 630000, Too many requests sent at the same time |
| Interactive Brokers | Web API (opens in a new tab): 10 requests a second per session; lower on some endpoints, such as 1 per 5 seconds on /iserver/orders | 429; an IP can go in a penalty box for 10 minutes |
| Schwab | Order limit setting of 0 to 120 per account per minute; others not readable | Not readable |
| tastytrade | No published per-endpoint quota | 429 with no rate headers; failed logins (opens in a new tab) can block an IP for about 8 hours |
| TradeStation | 320 per 5 minutes each for accounts, order details, balances and positions; 500 per 5 minutes for quote snapshots; 90 a minute per options endpoint; no quota listed for placing orders | 429, with X-RateLimit-Reset in seconds |
| Tradier | Per token, per minute: 120 standard, 120 market data, 60 trading; sandbox 60 each | 400, body Quota Violation: Expires <epoch-ms> |
| Webull | Order calls 600 per 60 seconds; preview 150 per 10 seconds; balances, positions and order detail 2 per 2 seconds | 429; repeats may bring an IP block |
Tradier's limits page and a no-token call both show the non-standard content type plain/text, so parse those replies as text.
Retrying orders safely: ID fields and what a 200 means
A timeout after sending an order leaves its fate unknown, and the brokers' documents differ on what a resend does, so look the order up before sending it again.
| Broker | Your ID field | What the docs add |
|---|---|---|
| Alpaca | client_order_id, up to 128 characters | Called unique; duplicate reply not described. alpaca-py resends on 429 and 504 by default |
| E*TRADE | clientOrderId, up to 20 alphanumeric characters | Meant to stop duplicates and unique per account, but the docs say it appears in no API response, so it cannot serve as a lookup key. Place Order needs the previewId from Preview Order, within 3 minutes |
| Schwab | None found in public material | Successful order calls usually return no JSON, per the community wrapper |
| tastytrade | external-identifier | The API does not deduplicate, so a repeated ID can place a second order. /orders/dry-run (opens in a new tab) validates and routes nothing |
| TradeStation | OrderConfirmID, 1 to 22 characters | Specification (opens in a new tab): prevents duplicates, unique per API key, per order, per user. Not in the order read fields; repeat reply not described |
| Tradier | tag, optional | A 200 can arrive even if a downstream system rejects the order; preview=true (opens in a new tab) validates only |
| Webull | client_order_id, required, up to 32 characters, unique per account | Resend behavior not described; preview is listed as the first step, not a stated requirement |
This stock trading API Python sample handles both traps, tested on synthetic replies on 5 November 2026 with Python 3.11.3:
import re, time
def limit_wait(status, headers, body, now=None):
"""Seconds to wait if this reply means 'rate limited', else None."""
now = time.time() if now is None else now
if status == 429: # Alpaca, IBKR, TradeStation, tastytrade, Webull
wait = headers.get("Retry-After", "60")
return float(wait) if wait.isdigit() else 60.0 # it can also be a date
if status == 400 and "Quota Violation" in body: # Tradier
expires = re.search(r"Expires\s+(\d+)", body)
return max(0.0, int(expires.group(1)) / 1000 - now) if expires else 60.0
return None
class Unknown(Exception):
"""The order's fate is not known: look it up, do not resend."""
def place(send, lookup, client_id):
"""Send once. On a timeout or 5xx, ask the broker what happened."""
try:
status, order = send(client_id)
except TimeoutError: # have send() map requests/httpx timeouts to this
status, order = 599, None
if status < 300:
return lookup(client_id) or order # a 200 alone proves little
if status >= 500:
found = lookup(client_id)
if found is None:
raise Unknown(client_id)
return found
raise ValueError(f"rejected: HTTP {status}")
if __name__ == "__main__":
NOW = 1_793_900_000.0
cases = [(429, {"Retry-After": "7"}, "", "429 with Retry-After"),
(429, {}, "", "429, no headers"),
(429, {"Retry-After": "Thu, 05 Nov 2026 18:30:00 GMT"}, "", "429, date header"),
(400, {}, f"Quota Violation: Expires {int((NOW + 12) * 1000)}", "Tradier quota"),
(400, {}, "Invalid Parameter", "ordinary 400")]
for status, headers, body, label in cases:
print(f"{label:22} wait={limit_wait(status, headers, body, NOW)}")
book = {} # synthetic broker
def lost_reply(cid): # order reached the book, reply lost
book[cid] = {"status": "open"}
raise TimeoutError
def never_arrived(cid):
raise TimeoutError
def downstream_reject(cid): # 200, then rejected downstream
book[cid] = {"status": "rejected"}
return 200, {"status": "ok"}
for send, cid in ((lost_reply, "a1"), (downstream_reject, "a2"), (never_arrived, "a3")):
try:
print(send.__name__, "->", place(send, book.get, cid))
except Unknown as err:
print(send.__name__, "-> Unknown, no resend:", err)Output:
429 with Retry-After wait=7.0
429, no headers wait=60.0
429, date header wait=60.0
Tradier quota wait=12.0
ordinary 400 wait=None
lost_reply -> {'status': 'open'}
downstream_reject -> {'status': 'rejected'}
never_arrived -> Unknown, no resend: a3The 60-second fallback is an illustrative setting. TradeStation's X-RateLimit-Reset counts seconds; Alpaca's Broker API (opens in a new tab) uses that name for a Unix timestamp. The lookup is broker-specific, and on a listing endpoint it may lag behind the order; more in client order ID.
Tokens: what expires, and who has to log in again
Token life decides whether a program can run unattended, and the range runs from no expiry to every day.
| Broker | What expires |
|---|---|
| Alpaca | Key pairs: no expiry stated on the pages read |
| E*TRADE | Access token ends at midnight US Eastern by default and goes inactive after two hours without requests; a renew call exists |
| Interactive Brokers | Gateway session up to 24 hours, with browser login on the same machine; times out (opens in a new tab) after about 6 minutes without requests or /tickle |
| Schwab | About 30 minutes and seven days, community-documented; see the Schwab guide |
| tastytrade | Access token 15 minutes; refresh tokens do not expire |
| TradeStation | Access token 20 minutes. Refresh tokens do not expire by default. Rotation on request: one page (opens in a new tab) says every 30 minutes, the auth overview every 40, with a 24-hour absolute limit |
| Tradier | Personal tokens last until regenerated |
| Webull | Requests are signed each time; a reusable token verified in the app is needed only with two-factor authentication |
Hypothetical design check. A trading program restarts at 09:00 US Eastern each trading day. On E*TRADE that restart meets a token that ended at midnight, so the daily runbook needs a login step. On tastytrade the same restart can refresh by itself.
Does Robinhood have an API? Webull, E*TRADE and the rest
Robinhood documents a crypto REST API and a separate MCP connection for AI agents, and no REST API for stocks was found.
- Crypto Trading API. Its support article describes crypto market data, account data and orders for US Robinhood Crypto customers, and does not mention stocks or options.
- Trading MCP. Support pages describe connecting an external AI agent to
agent.robinhood.com/mcp/trading, with equities, options and crypto tools. Trades go only to a dedicated agentic account, the agent gets read access to all your accounts, and you stay responsible for its trades. This was read, not run. - Community libraries.
robin_stocks(opens in a new tab) (3.4.0, 18 May 2025) lists an individual author and does not claim a link to Robinhood. Read the terms before relying on any unofficial route; this guide does not cover them.
tastytrade from Python. Its own Python SDK repository (opens in a new tab) was archived on 13 March 2026, and the tastytrade (opens in a new tab) package on PyPI (13.2.3, 7 August 2026) calls itself unofficial.
For readers in Turkey on the crypto point: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.
How to choose a stock trading API without a ranking
Work through these in order, because the first two can rule a broker out before any code is written.
- Eligibility. Ask the broker in writing whether your country and account type can use the API.
- Use. Own account or customers? Serving others is a separate product and a licensing question, as in Alpaca's Trading and Broker APIs.
- Login routine. Can it run unattended, or does a person log in daily or weekly?
- Test environment. Which failures can it actually produce?
- Retry rules. Is there a client ID, and can you search by it?
- Data rights. Can you display what you receive to others?
- Protocol. Volume may justify FIX; see QuickFIX Python.
Rules depend on your country, and account types and API access vary. For readers in Turkey, the Capital Markets Board (SPK) said on 23 February 2023 that residents can trade leveraged forex only through institutions it has authorized, and Law No. 6362 (opens in a new tab) provides access blocks for online offers from abroad (Article 99(4)) and criminal penalties for unauthorized activity (Article 109(2)).
Fincode quotes this work through exchange API integration and claims no prior work with any broker named here.
Frequently asked questions
Does Robinhood have an API?
Its published REST API covers crypto for US customers, and its support pages describe a Trading MCP connection for AI agents. No REST API for stocks was found.
Does E*TRADE have an API?
Yes. Its documents, read on its sandbox host, describe signed OAuth requests, a required preview before placing an order, and access tokens that end at midnight US Eastern by default.
Is there a Python SDK for stock trading APIs?
Alpaca publishes alpaca-py and Webull publishes webull-openapi-python-sdk. schwab-py is a community wrapper for Schwab. tastytrade's own SDK repository is archived and the tastytrade package on PyPI calls itself unofficial, so otherwise it means plain HTTP.
Can I test a stock trading API without a funded account?
Alpaca's docs allow a paper-only account for anyone globally, and Webull's offer shared test accounts with no application. TradeStation's key request needs a funded account, and Tradier's docs ask for a brokerage account.
Sources
- Tradier Docs: Rate limits (opens in a new tab)
- Tradier Docs: Getting started (hosts, tokens, order confirmation) (opens in a new tab)
- TradeStation API: SIM vs LIVE (opens in a new tab)
- TradeStation API: Rate limiting overview (opens in a new tab)
- TradeStation API: Authentication overview (opens in a new tab)
- tastytrade Developer Docs: Idempotency and retries (opens in a new tab)
- tastytrade Developer Docs: Sandbox (opens in a new tab)
- tastytrade Developer Docs: OAuth2 (opens in a new tab)
- E*TRADE API documents: Get Access Token (opens in a new tab)
- E*TRADE API documents: Order API (opens in a new tab)
- Webull OpenAPI: Rate limits (opens in a new tab)
- Webull OpenAPI: Stock orders (client_order_id) (opens in a new tab)
- Robinhood Support: Crypto Trading API (opens in a new tab)
- Robinhood Support: Onboarding an external agent (Trading MCP) (opens in a new tab)
- SPK (Capital Markets Board): Notice on unauthorized leveraged transactions, 23 February 2023 (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.