schwabdev Tutorial — Charles Schwab API in Python
A practical, source-code-level guide to the wrapper that gets you from zero to quotes, option chains, orders and WebSocket streaming with the least infrastructure.
schwabdev is a lightweight but opinionated wrapper: it manages tokens for you, stores them in SQLite, can encrypt them, exposes both synchronous and asynchronous clients, and ships a streamer with reconnect logic. It is MIT-licensed and maintained by Tyler Bowers and contributors. It is not an official Schwab SDK.
1. Install and configure
python -m venv .venv # Windows: .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate pip install schwabdev==4.0.0 python-dotenv
Requirements: Python 3.11+, requests, aiohttp, websockets, cryptography, tzdata. Create a .env:
SCHWAB_APP_KEY=your-app-key SCHWAB_APP_SECRET=your-app-secret SCHWAB_CALLBACK_URL=https://127.0.0.1:8182
The callback URL must exactly match the developer portal. The wrapper rejects non-HTTPS callbacks and callback URLs ending in /.
2. Client and token lifecycle
import os
from dotenv import load_dotenv
import schwabdev
load_dotenv()
client = schwabdev.Client(
os.environ["SCHWAB_APP_KEY"],
os.environ["SCHWAB_APP_SECRET"],
os.environ["SCHWAB_CALLBACK_URL"],
)
accounts = client.linked_accounts().json()
account_hash = accounts[0]["hashValue"]
The default token database is ~/.schwabdev/tokens.db. On first run, the client starts the authorization flow. It treats the access token as a 30-minute credential and the refresh token as a seven-day credential. update_tokens() refreshes the access token when it is within about 61 seconds of expiry; the refresh token is renewed through the authorization flow when it is within about 60 minutes of its seven-day deadline. The session header is updated after each refresh.
To move the token database:
client = schwabdev.Client(
os.environ["SCHWAB_APP_KEY"],
os.environ["SCHWAB_APP_SECRET"],
os.environ["SCHWAB_CALLBACK_URL"],
tokens_db="/secure/path/schwab-tokens.db",
)
To encrypt tokens at rest, generate a Fernet key and pass it as encryption=. Store the key outside the database and outside Git. Losing it requires re-authentication.
3. Accounts and positions
all_accounts = client.account_details_all().json()
positions = client.account_details(
account_hash,
fields="positions",
).json()
orders = client.account_orders(
account_hash,
"2026-09-01T00:00:00Z",
"2026-10-30T23:59:59Z",
).json()
Use linked_accounts() to map account numbers to hashes. Order and position endpoints expect the hash, not the account number. Date arguments accept datetime objects or ISO strings; use UTC.
4. Quotes, price history and option chains
quotes = client.quotes(["AAPL", "MSFT", "SPY"]).json()
history = client.price_history(
"AAPL",
periodType="year", period=1,
frequencyType="daily", frequency=1,
).json()
chain = client.option_chains(
"AAPL",
contractType="CALL",
strikeCount=10,
includeUnderlyingQuote=True,
).json()
price_history() supports period/frequency combinations and explicit start/end dates. option_chains() exposes contract type, strike count, strategy, strike range, expiration window, volatility, underlying price, interest rate and entitlement. Constrain the chain: a full index chain can be very large and can consume your market-data budget.
Other useful methods:
| Method | Use |
|---|---|
quote(symbol) | Single-symbol quote. |
option_expiration_chain(symbol) | Available expirations. |
movers(index) | Top movers for an index. |
market_hours(symbols) | Market hours by product. |
instruments(symbols, projection) | Instrument metadata and fundamentals. |
transactions(account_hash, start, end, types) | Account transaction history. |
5. Orders: preview, place, cancel
schwabdev accepts the raw Schwab order JSON. That is powerful and unforgiving: a missing field is rejected by the API.
order = {
"orderType": "LIMIT",
"session": "NORMAL",
"duration": "DAY",
"orderStrategyType": "SINGLE",
"price": "100.00",
"orderLegCollection": [{
"instruction": "BUY",
"quantity": 1,
"instrument": {"symbol": "AAPL", "assetType": "EQUITY"},
}],
}
preview = client.preview_order(account_hash, order).json()
resp = client.place_order(account_hash, order)
order_id = resp.headers.get("location", "/").split("/")[-1]
client.cancel_order(account_hash, order_id)
# client.replace_order(account_hash, order_id, order)
Always preview. Persist the order ID. If place_order() times out, do not resend blindly: query the order list and reconcile by symbol, side, quantity and time. For options, use the exact OCC symbol and verify the account's options approval level.
6. Streaming with Stream
streamer = schwabdev.Stream(client)
def on_message(message):
print(message)
streamer.start(on_message)
streamer.send(
streamer.level_one_equities(
"AAPL,MSFT",
"0,1,2,3,4,5,6,7,8",
)
)
# later
streamer.stop()
The streamer logs in with credentials from the Trader API, then subscribes. Available helpers include:
level_one_equities,level_one_options,level_one_futures,level_one_futures_options,level_one_forexnyse_book,nasdaq_book,options_bookchart_equity,chart_futuresscreener_equity,screener_optionsaccount_activity
Commands are ADD, SUBS, UNSUBS and VIEW. Up to 500 keys can be streamed. The streamer includes reconnect/backoff logic and can automatically start and stop around market hours with start_auto().
7. Async with ClientAsync and StreamAsync
import asyncio
import schwabdev
async def main():
async with schwabdev.ClientAsync(
os.environ["SCHWAB_APP_KEY"],
os.environ["SCHWAB_APP_SECRET"],
os.environ["SCHWAB_CALLBACK_URL"],
) as client:
symbols = ["AAPL", "MSFT", "NVDA", "AMD"]
async def fetch(symbol):
resp = await client.quotes(symbol)
return await resp.json()
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(s)) for s in symbols]
print([t.result() for t in tasks])
asyncio.run(main())
ClientAsync uses aiohttp and runs a background token-refresh task. StreamAsync provides the async streamer. Async is useful for quote fan-out and dashboards; it does not increase the API's rate limits.
8. Operational notes
- Rate limits: the client retries 429 and 500/502/503/504, but retries are not a substitute for a request budget. Batch quotes and cache instrument metadata.
- Token races: the SQLite token store coordinates multiple client instances. Do not copy the DB while a refresh is in progress.
- Logging: enable logging at INFO during setup, then reduce it. Never log the token or app secret.
- No paper trading: the Schwab API has no paper environment. Use small live size, a separate account, or simulated fills.
- Production: run the streamer in its own process, reconcile via REST on reconnect, and persist orders before acting on a socket event.
9. FAQ
Is schwabdev official?
No. It is a community project, MIT-licensed and not affiliated with Charles Schwab.
Does schwabdev work with Python 3.10?
No. The package declares Python 3.11+.
Can I use schwabdev without the token database?
You can change the path, but the client still uses SQLite. If you need a custom store, use schwab-py access functions or wrap the token flow yourself.
How many symbols can I stream?
Up to 500 keys per the streamer's subscription design, subject to Schwab's streaming entitlements and message rates.
Schwab API Python series: Hub | Full tutorial | Setup & OAuth | schwabdev | schwab-py | Comparison
Need a schwabdev-Based Bot or Dashboard?
We build Python scanners, execution engines and streaming dashboards with token security, reconciliation and risk controls.
Discuss Your BuildReference: schwabdev on GitHub · PyPI. Version referenced: 4.0.0. 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.