Free learning tools Test your finance and trading knowledge before you build, buy, or automate.
schwabdev 4.0.0

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:

MethodUse
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_forex
  • nyse_book, nasdaq_book, options_book
  • chart_equity, chart_futures
  • screener_equity, screener_options
  • account_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 Build

Reference: schwabdev on GitHub · PyPI. Version referenced: 4.0.0. Unofficial, MIT-licensed, not affiliated with Charles Schwab. Not financial advice.

🚀 Invite friends — Earn $5 You both get $5 credit on Go Ad · opencode.ai AI-Powered Coding Agent — Try Free Build apps, fix bugs & ship faster with opencode. Get $5 free credit when you join. × Ad · quo.com QuoPhone — $20 Visa Gift Card Free Sign up to Quo, subscribe 3 months, get a $20 Visa gift card. Atif's referral gift for you. ×