Free learning tools Test your finance and trading knowledge before you build, buy, or automate.
schwab-py 1.5.1

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.

FunctionWhen 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.
Token file rules: do not create or hand-edit it. It contains metadata used to compute token age. Set restrictive permissions (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.

MethodPurpose
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)
Order safety: preview, place, persist the order ID, then reconcile. A timeout on 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/exceptionMeaningAction
401Token missing/expiredRefresh or re-authenticate.
403App/account entitlement missingCheck approval and permissions; do not loop.
429Rate limitBackoff with jitter; batch requests.
UnsuccessfulOrderExceptionOrder rejectedRead the API error body; fix the order; preview again.
AccountHashMismatchExceptionOrder/account hash mismatchUse 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 Project

Reference: schwab-py on GitHub · PyPI · documentation. Version referenced: 1.5.1. 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. ×