schwab_sdk/lib.rs
1//! A typed Rust client for the Charles Schwab Trader API, Market Data APIs, and
2//! streaming data.
3//!
4//! It provides access to every endpoint via a namespace accessor on
5//! [`SchwabClient`]. With it you can:
6//!
7//! - [List linked accounts, balances, and their positions](`accounts`)
8//! - [Query quotes, price history, options chains, and other market data](`market_data`)
9//! - [Stream real-time market data and account activity](`streamer`)
10//! - [Place, replace, cancel, and preview orders](`orders`)
11//! - [List transactions](`transactions`)
12//! - [Read user preferences](`user_preferences`)
13//!
14//! All money and quantity fields use [`rust_decimal::Decimal`]. Secrets
15//! ([`AuthToken`], [`CustomerId`], [`AccountNumber`], [`AccountHash`])
16//! are wrapped in newtypes that redact in `Debug` and zeroize on `Drop`.
17//!
18//! To start, you will need to obtain an access token. See the
19//! [Authentication](#authentication) section for details.
20//!
21//! # Print the number of linked accounts
22//!
23//! The client makes it simple to access the Schwab API through a fluent interface.
24//!
25//! ```no_run
26//! use schwab_sdk::{AuthToken, SchwabClient};
27//!
28//! # async fn run() -> schwab_sdk::Result<()> {
29//! // Construct an access token from an environment variable and create a
30//! // client.
31//! let token = AuthToken::new(std::env::var("SCHWAB_ACCESS_TOKEN").unwrap());
32//! let client = SchwabClient::new(token);
33//!
34//! // Retrieve a list of linked accounts and their account numbers.
35//! let accounts = client.accounts().numbers().await?;
36//! println!("{} linked account(s)", accounts.len());
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! ## Read a quote and place an order
42//!
43//! This example requests a quote for AAPL, places a limit buy order just under
44//! the last trade, and prints the order id. Orders are constructed using the
45//! [`orders::OrderRequest`] builder, which enforces type safety and compile-time
46//! validation.
47//!
48//! ```no_run
49//! use rust_decimal_macros::dec;
50//! use schwab_sdk::{AuthToken, SchwabClient};
51//! use schwab_sdk::market_data::QuoteEntry;
52//! use schwab_sdk::orders::OrderRequest;
53//!
54//! # async fn run() -> schwab_sdk::Result<()> {
55//! let client = SchwabClient::new(AuthToken::new("token"));
56//!
57//! // 1. Resolve the encrypted account hash. Every per-account endpoint
58//! // takes this hash in its `{accountNumber}` path segment, never the
59//! // plain account number.
60//! let accounts = client.accounts().numbers().await?;
61//! let account = accounts.first().expect("at least one linked account");
62//! let account_hash = &account.hash_value;
63//!
64//! // 2. Read a quote and pull the last price out of the typed entry. An
65//! // unknown symbol comes back as `QuoteEntry::Error`, not an `Err`.
66//! let quotes = client.market_data().quotes().list(["AAPL"]).send().await?;
67//! let last_price = match quotes.get("AAPL") {
68//! Some(QuoteEntry::Equity(q)) => q.quote.as_ref().and_then(|inner| inner.last_price),
69//! _ => None,
70//! };
71//! let Some(last_price) = last_price else {
72//! println!("no quote for AAPL");
73//! return Ok(());
74//! };
75//!
76//! // 3. Place a limit buy just under the last trade; keep the order id
77//! // Schwab returns so the fill can be polled later.
78//! let limit = last_price - dec!(0.50);
79//! let order_id = client
80//! .orders(account_hash)
81//! .place(OrderRequest::buy_limit("AAPL", dec!(10), limit))
82//! .await?;
83//! println!("placed order {order_id} at {limit}");
84//! # Ok(())
85//! # }
86//! ```
87//!
88//! # Authentication
89//!
90//! The Schwab APIs require a short-lived access token. You will need to obtain
91//! one using Schwab's OAuth flow and either pass it to [`SchwabClient::new`]
92//! or make it available to the client via a [`TokenProvider`]. The
93//! `TokenProider` is the recommended mechanism for long-lived clients. The
94//! provider is consulted once per REST request and once per streamer LOGIN frame,
95//! so a rotated token is observed on the next call without rebuilding the client.
96//!
97//! See the [`TokenProvider`] docs for examples of implementing a custom
98//! provider.
99//!
100//! **Note:** This crate does not perform the OAuth authorization-code exchange
101//! See [Schwab's developer portal](https://developer.schwab.com/)
102//! for details on their OAuth flow.
103//!
104//! # Out of scope
105//!
106//! - The OAuth authorization-code flow. Callers obtain a bearer token
107//! out of band and hand it to [`SchwabClient::new`], or implement
108//! [`TokenProvider`] for refresh-on-demand (see its doctest for a
109//! worked provider).
110//! - Retry and rate limiting. Each [`Error`] exposes
111//! [`Error::is_retryable`] and [`Error::retry_after`] so a caller can
112//! layer a policy (`backon`, etc.) on top. See the doctest on
113//! [`Error::is_retryable`] for a minimal backoff loop.
114//! - Idempotent order submission. Place / replace / cancel / preview exist,
115//! but the Schwab API exposes no client-controllable idempotency key;
116//! callers that need retry-safe submission must dedupe at their own
117//! layer.
118//!
119//! # Security
120//!
121//! `schwab-sdk` is built to reduce the risk of credential or PII
122//! leakage through this crate, not to be a security boundary for the
123//! application as a whole.
124//!
125//! - The secret newtypes ([`AuthToken`], [`CustomerId`],
126//! [`AccountNumber`], [`AccountHash`]) redact in `Debug` and zeroise
127//! on `Drop`. The [`secrets`] module documents what these properties
128//! cover and what they do not.
129//! - The crate emits no log lines, writes no files, and does not embed
130//! secret values in [`Error`] variants. A bearer credential is
131//! materialised only at the `Authorization` header and the streamer
132//! LOGIN frame.
133//! - Transport defaults to HTTPS for REST and WSS for the streamer.
134//! Release builds reject `http://` base-URL overrides and `ws://`
135//! streamer URLs; debug builds permit them so local fixture servers
136//! work in tests.
137//! - Credential storage, the OAuth flow, retry policy, rate limiting,
138//! structured logging, and host-level hardening (disabling core
139//! dumps, encrypted swap) are caller responsibilities. See
140//! `SECURITY.md` in the repository for the vulnerability-reporting
141//! channel and the formal scope.
142//!
143//! # Disclaimer
144//!
145//! This crate is an independent client. It is **not affiliated with,
146//! endorsed by, or sponsored by Charles Schwab & Co., Inc.** "Schwab"
147//! and related marks are the property of their respective owners.
148//!
149//! The crate is provided "as is" without warranty of any kind. The
150//! authors and contributors are **not responsible for any financial
151//! loss, missed trades, incorrect or duplicate orders, or other trading
152//! outcomes** arising from use of this crate. You are solely responsible
153//! for the orders your code submits and for verifying its behavior
154//! before trading real money. See the MIT and Apache-2.0 license texts
155//! for the full warranty disclaimer.
156
157// Panic-family lints are denied in production code. If a future change
158// genuinely needs one of these in non-test code, add `#[allow(...)]` with
159// a one-line comment explaining why.
160#![cfg_attr(
161 not(test),
162 deny(
163 clippy::unwrap_used,
164 clippy::expect_used,
165 clippy::panic,
166 clippy::unreachable,
167 clippy::todo,
168 clippy::unimplemented,
169 )
170)]
171#![cfg_attr(docsrs, feature(doc_cfg))]
172#![warn(missing_docs)]
173
174mod client;
175mod constants;
176mod token;
177
178pub(crate) mod macros;
179
180pub mod accounts;
181pub mod error;
182pub mod market_data;
183pub mod orders;
184pub mod secrets;
185pub mod streamer;
186pub mod transactions;
187pub mod user_preferences;
188
189// Re-exports of dependencies whose types appear in the public API.
190pub use chrono;
191pub use http;
192pub use rust_decimal;
193
194/// Construct a [`rust_decimal::Decimal`] from a numeric literal at compile time.
195///
196/// Re-export of [`rust_decimal_macros::dec`](https://docs.rs/rust_decimal/latest/rust_decimal/macro.dec.html).
197///
198/// ```
199/// # #[cfg(feature = "macros")] {
200/// use schwab_sdk::dec;
201///
202/// let price = dec!(123.45);
203/// # let _ = price;
204/// # }
205/// ```
206#[cfg(feature = "macros")]
207pub use rust_decimal_macros::dec;
208
209pub use client::SchwabClient;
210pub use constants::{
211 DEFAULT_AUTH_TOKEN_EXPIRY, DEFAULT_REFRESH_TOKEN_EXPIRY, MARKET_DATA_BASE_URL, TRADER_BASE_URL,
212};
213pub use error::{Error, ErrorBody, Result};
214pub use secrets::{AccountHash, AccountNumber, AuthToken, CustomerId};
215pub use streamer::{StreamerResponse, WebSocketError};
216pub use token::{StaticTokenProvider, TokenProvider};