schwab-py Tutorial — Charles Schwab API Python Client
The thin, typed wrapper: safe OAuth, token files, explicit enums, order builders and an async WebSocket client — with the exact method names and response behavior.
schwab-py is intentionally unopinionated. It wraps each Schwab endpoint with a small Python method, returns the raw httpx response and leaves interpretation to you. That is a feature: when Schwab changes a field or an order shape, you are not fighting a second abstraction layer. It is MIT-licensed and not affiliated with Schwab.
1. Install and configure
python -m venv .venv # Windows: .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate pip install schwab-py==1.5.1 python-dotenv
Requirements: Python 3.10+, authlib, httpx, flask, websockets, psutil, python-dateutil. The callback URL must exactly match the developer portal; for the browser flow use https://127.0.0.1:8182.
2. Authentication: easy_client and the other flows
import os
from schwab import auth
client = auth.easy_client(
os.environ["SCHWAB_APP_KEY"],
os.environ["SCHWAB_APP_SECRET"],
os.environ["SCHWAB_CALLBACK_URL"],
".tokens/schwab-token.json",
)
print(client.get_account_numbers().json())
easy_client() is the default entry point. It loads the token file if present; otherwise it opens the browser, starts a local callback server and writes a new token. The default max_token_age is 6.5 days, so a token close to the seven-day refresh boundary is proactively replaced.
| Function | When to use it |
|---|---|
easy_client() | Normal desktop/server startup with a browser available on first run. |
client_from_login_flow() | Explicit browser flow with a callback timeout and custom browser. |
client_from_token_file() | Headless server: token created elsewhere, copied securely. |
client_from_manual_flow() | Notebooks or environments without a local callback server. |
client_from_access_functions() | Custom token storage: database, secret manager, serverless. |
chmod 600), keep it out of Git and encrypt backups.3. Accounts, quotes and price history
accounts = client.get_account_numbers().json()
account_hash = accounts[0]["hashValue"]
quote = client.get_quote("AAPL")
quote.raise_for_status()
print(quote.json())
history = client.get_price_history_every_day("AAPL")
history.raise_for_status()
bars = history.json()["candles"]
Price-history convenience methods exist for one-, five-, ten-, fifteen- and thirty-minute bars, plus daily and weekly bars. For custom ranges use get_price_history() with start_datetime and end_datetime. Timestamps are milliseconds; convert them explicitly before joining with other datasets.
| Method | Purpose |
|---|---|
get_accounts(), get_account() | Balances, positions and orders. |
get_quotes(), get_quote() | Batch or single quotes. |
get_option_chain() | Constrained option chain. |
get_option_expiration_chain() | Available expirations. |
get_movers(), get_market_hours() | Index movers and session hours. |
get_instruments(), get_instrument_by_cusip() | Instrument metadata. |
4. Option chains with typed enums
chain = client.get_option_chain(
"AAPL",
contract_type=client.Options.ContractType.CALL,
strike_count=10,
include_underlying_quote=True,
)
chain.raise_for_status()
data = chain.json()
Enums are enforced by default. If you disable enforce_enums, you must pass the exact string values the API expects. Keep enum enforcement on in production; it catches typos before a request is sent. Constrain chains by expiration, strike count, contract type and range to protect the market-data budget.
5. Orders: builders and raw payloads
from schwab.orders.equities import equity_buy_limit
from schwab.orders.options import bull_call_vertical_open
from schwab.utils import Utils
equity_order = equity_buy_limit("AAPL", 1, 100.00)
preview = client.preview_order(account_hash, equity_order)
preview.raise_for_status()
placed = client.place_order(account_hash, equity_order)
placed.raise_for_status()
order_id = Utils(client, account_hash).extract_order_id(placed)
The equity builders cover market/limit buys, sells, short sells and buy-to-cover. The options module includes single-leg opens/closes and vertical spreads such as bull call, bear call, bull put and bear put. The generic OrderBuilder supports complex strategies, child orders and one-cancels-other / first-triggers-second relationships.
For cancellation, the argument order is cancel_order(order_id, account_hash) — the reverse of the place call. Confirm this in the version you pin; wrapper signatures can change.
client.cancel_order(order_id, account_hash) client.replace_order(account_hash, order_id, equity_order)
place_order() does not prove the order failed. Query orders and positions before retrying.6. Async streaming with StreamClient
import asyncio
from schwab.streaming import StreamClient
async def main():
accounts = client.get_account_numbers().json()
account_id = int(accounts[0]["accountNumber"])
stream = StreamClient(client, account_id=account_id)
async def on_quote(message):
print(message)
stream.add_level_one_equity_handler(on_quote)
await stream.login()
await stream.level_one_equity_subs(["AAPL", "MSFT"])
while True:
await stream.handle_message()
asyncio.run(main())
StreamClient is fully asynchronous. You register one handler per service and drive the receive loop with handle_message(). Handlers exist for account activity, chart equity/futures, level-one equity/option/futures/forex/futures-options, NYSE/NASDAQ/options books and screeners. The library also exposes a custom JSON decoder for high-throughput parsing.
Streaming is a notification channel, not the source of truth. On reconnect, reconcile orders and positions through REST before acting on any risk rule.
7. Error handling and debugging
| Status/exception | Meaning | Action |
|---|---|---|
| 401 | Token missing/expired | Refresh or re-authenticate. |
| 403 | App/account entitlement missing | Check approval and permissions; do not loop. |
| 429 | Rate limit | Backoff with jitter; batch requests. |
UnsuccessfulOrderException | Order rejected | Read the API error body; fix the order; preview again. |
AccountHashMismatchException | Order/account hash mismatch | Use the hash from get_account_numbers(). |
schwab-py returns raw responses; call raise_for_status() or inspect the status code. The debug module can redact API keys and tokens from logs; use it when capturing bug reports.
8. Limitations to plan around
- No paper trading through the API. Use small live size or simulated fills.
- No historical options pricing. Collect forward snapshots if you need an options dataset.
- Seven-day refresh token. A process that is offline at expiry needs a new browser/manual flow.
- No official support. Pin the library version and read the changelog before upgrading.
9. FAQ
Is schwab-py better than schwabdev?
It is thinner and more explicit. If you want typed order builders and raw endpoint access, it is better. If you want automatic token encryption and a managed streamer, schwabdev is faster.
Can I use schwab-py in an async web service?
Yes. easy_client(..., asyncio=True) creates an async client, and StreamClient is async-native.
Does it support options orders?
Yes, including single-leg and vertical spread builders. Your Schwab account still needs the corresponding options approval level.
Schwab API Python series: Hub | Full tutorial | Setup & OAuth | schwabdev | schwab-py | Comparison
Need a Typed Schwab Order Engine?
We build schwab-py execution services with previews, order builders, reconciliation, risk checks and async streaming.
Discuss Your ProjectReference: schwab-py on GitHub · PyPI · documentation. Version referenced: 1.5.1. Unofficial, MIT-licensed, not affiliated with Charles Schwab. Not financial advice.
Go from article to implementation
Read the proof, then test the idea with calculators, free tools, case studies and a fixed-price build path.
Go deeper before you build, download or automate
Hand-picked engineering articles, indicator breakdowns and trading-system tools so visitors keep exploring instead of leaving after one page.
Pine Script to MT4/MT5 Conversion Planner
Analyze Pine strategies, repainting risk, TradingView alert bridges, MTF synchronization and the correct MQL4/MQL5 build path before paying for conversion.
Open planner
3-in-1 MT5 Indicator Bundle
12H Bias Engine, Smart Money Institutional Flow Dashboard and D1/H1 pending-signal logic with MQ5 + EX5 source included via Etsy.
MT5 Bank Orders Institutional Flow Dashboard
Supply/demand zones, BOS/CHoCH, liquidity levels, FVG, equal highs/lows and confirmed trade-plan logic explained with EX5 download.
MT5 Asia London NY Liquidity Engine
Asia range, London raid and New York continuation/reversal planning with internal session backtest stats and entry/SL/TP dashboard.
Interactive Brokers RSI Pair Trader
Tkinter desktop trading workflow, TWS API connection handling, RSI pair logic, risk controls and custom IBKR development positioning.
XAUUSD M30 CSV Data For Backtesting
Use clean MT5 historical data for Python backtests, strategy validation, spread checks and realistic gold EA research workflows.