A Rust SDK for Charles Schwab's Trading and Market Data APIs, providing OAuth2 authentication, REST API access, and WebSocket streaming capabilities.
Warning
RISK WARNING: This SDK is for advanced users and is provided "as is" without warranty of any kind. Automated trading involves significant risk of financial loss. Always test your logic with a test account or "Paper Money" environment before using real capital. Use of this SDK is entirely at your own risk.
- Full Documentation Index: Main entry point for all guides.
- Setup Guide: Detailed instructions for getting started.
- Placing Orders: Comprehensive guide for trading equities and options.
- Real-time Streaming: Deep dive into the WebSocket client and services.
- Troubleshooting: Common issues and solutions.
- Developer Documentation: For contributors and architecture review.
-
OAuth2 Authentication
- Authorization code flow with built-in callback server
- Automatic token refresh with configurable buffer
- 7-day refresh token expiration handling with notifications
- HTTPS validation and security enforcement
- HTTP client session management
- Token file corruption recovery
-
Security & Token Storage
- Memory-safe token handling with automatic zeroing (SecretString)
- Three storage backends: File (basic), EncryptedFile (ChaCha20Poly1305), Keychain (OS-native)
- Secure by default: macOS uses Keychain, other platforms use EncryptedFile
- File permissions: Unix 0600 enforcement on token files
- OWASP compliant: Best-in-class encryption and key derivation (PBKDF2 600k iterations)
-
Market Data APIs
- Real-time quotes and price history
- Advanced quote options: Filter by fields and enable
indicativequotes - Option chains with Greeks
- Market movers and instruments
- WebSocket streaming for live data
-
Trading APIs
- Account management
- Order placement and management
- Order Preview: Validate orders without execution
- Position tracking
- Transaction history
-
Runtime Resilience
- Rate limiting (120 req/s with burst of 20)
- Exponential backoff retry logic
- 90-second crash detection for streaming
- WebSocket Ping/Pong heartbeat monitoring (v0.2.0)
- Timeout detection with automatic reconnection (v0.2.0)
- Field-based subscription batching (scoped)
- Token expiry notifications
- Comprehensive error handling
- Type-safe bindings for commonly used endpoints (OpenAPI coverage in progress)
-
Streaming Advanced Features (v0.2.0)
- Bounded/Unbounded channels for backpressure control
- Automatic Ping/Pong heartbeat (20s interval, 30s timeout)
- Subscription persistence across reconnections
- Custom field selection per service
- All 13 streaming services fully implemented
This SDK implements world-class security with multiple layers of protection, significantly exceeding typical OAuth 2.0 implementations.
| Feature | rustyschwab | Python Reference | Advantage |
|---|---|---|---|
| Encryption at Rest | ✅ ChaCha20Poly1305 AEAD | ❌ Plaintext JSON | 100% improvement |
| Tamper Detection | ✅ AEAD authentication tags | ❌ None | Data integrity |
| Memory Protection | ✅ SecretString + Zeroize | ❌ Plain strings | Memory safety |
| Keychain Storage | ✅ macOS/Windows/Linux | ❌ Not implemented | Best-in-class |
| File Permissions | ✅ Enforced 0600 (Unix) | ❌ Default perms | Access control |
| PKCE (RFC 7636) | ✅ S256 method | ❌ Not implemented | OAuth 2.1 ready |
| Thread Safety | ✅ Compile-time safe | Concurrency | |
| Secret Logging | ✅ Redacted in logs | Audit safety |
Layer 1: Memory Safety
// Tokens wrapped in SecretString with automatic zeroization
pub struct TokenSet {
access_token: Option<SecretString>, // Auto-cleared on drop
refresh_token: Option<SecretString>, // Never logged
}- Prevents memory dumps from exposing tokens
- Not visible in core dumps or swap files
- Explicit
expose_secret()required for access - Compiler prevents accidental logging
Layer 2: Encryption at Rest
// ChaCha20Poly1305 AEAD (authenticated encryption)
- Algorithm: ChaCha20Poly1305 (NCC Group audited, 2020)
- Key: 256-bit random (OS CSPRNG)
- Nonce: 96-bit random per encryption
- Authentication: 128-bit tag prevents tampering
Layer 3: OS-Native Credential Storage
// Cross-platform secure storage
- macOS: Keychain (secure enclave integration)
- Windows: Credential Manager (DPAPI encryption)
- Linux: Secret Service API (GNOME Keyring/KWallet)
Layer 4: File Permissions
// Unix: Enforced 0600 (owner read/write only)
- Verified before every read
- Applied after every write
- Prevents unauthorized access on shared systems
Layer 5: Transport Security
// TLS/HTTPS enforcement
- rustls: Memory-safe TLS (no OpenSSL vulnerabilities)
- HTTPS-only callback URLs (validated at config time)
- WSS for streaming (TLS over WebSocket)
Protect against authorization code interception attacks:
use schwab_rs::auth::OAuthConfig;
let oauth = OAuthConfig {
app_key: "YOUR_APP_KEY".into(),
app_secret: "YOUR_SECRET".into(),
callback_url: "https://127.0.0.1:8080".into(),
pkce_enabled: true, // Enabled by default (OAuth 2.1 compliant)
..Default::default()
};PKCE Security Benefits:
- Code verifier: 256-bit random value (OS CSPRNG)
- Code challenge: SHA-256 hash with base64url encoding
- Method: S256 (cryptographically secure)
- Protection: Prevents authorization code interception/replay attacks
1. Choose the Right Token Storage Backend
use schwab_rs::auth::TokenStoreKind;
// Development: Quick start with basic security
token_store_kind: TokenStoreKind::File, // 0600 permissions only
// Production (Recommended): Encrypted file storage
token_store_kind: TokenStoreKind::EncryptedFile, // ChaCha20Poly1305 AEAD
// Production (Best): OS-native credential storage
token_store_kind: TokenStoreKind::Keychain, // Default on macOSSecurity Levels:
- File: Basic (0600 permissions) - Development/testing only
- EncryptedFile: High (AES-equivalent encryption + tamper detection) - Production recommended
- Keychain: Best (OS-level encryption + user authentication) - Production default on macOS
2. Enable Token Notifications
use schwab_rs::auth::{OAuthConfig, TokenNotification};
use std::sync::Arc;
let oauth = OAuthConfig {
on_token_notification: Some(Arc::new(|notification| {
match notification {
TokenNotification::RefreshTokenExpiring { hours_remaining } => {
eprintln!("⚠️ Refresh token expires in {} hours!", hours_remaining);
// Send alert, log to monitoring, etc.
}
TokenNotification::TokenFileCorrupted => {
eprintln!("🔒 Token file was corrupted and recreated");
}
_ => {}
}
})),
..Default::default()
};3. Secure Your Credentials
# NEVER commit credentials to version control
echo "schwab_tokens.json" >> .gitignore
echo "schwab_tokens.json.key" >> .gitignore
echo ".env" >> .gitignore
# Use environment variables
export SCHWAB_APP_KEY="your_32_char_key"
export SCHWAB_APP_SECRET="your_16_char_secret"
# Or use a secret management service
# - AWS KMS Secrets Manager
# - HashiCorp Vault
# - Azure Key Vault4. Production Deployment Checklist
- Use
EncryptedFileorKeychainstorage backend - Ensure PKCE is enabled (
pkce_enabled: true, default) - Set up token expiry notifications
- Configure proper file permissions (0600 on Unix)
- Use HTTPS-only callback URLs
- Never log credentials or tokens
- Rotate credentials regularly
- Monitor for token expiry events
- Use rate limiting in production
- Enable retry logic with exponential backoff
ChaCha20Poly1305 AEAD (EncryptedFile Backend)
// Encryption process
1. Generate random 256-bit key (once, stored separately)
2. For each encryption:
- Generate random 96-bit nonce (OS CSPRNG)
- Encrypt plaintext with ChaCha20
- Compute Poly1305 authentication tag
- Output: [nonce(12) || ciphertext || tag(16)]
// Security properties
- Confidentiality: ChaCha20 stream cipher (256-bit key)
- Authenticity: Poly1305 MAC (128-bit tag)
- Tamper detection: Any modification causes decryption failure
PKCE Code Verifier/Challenge (RFC 7636)
// Code verifier generation
1. Generate 32 bytes random (256 bits) from OS CSPRNG
2. Base64URL encode without padding
3. Result: 43-character verifier string
// Code challenge generation
1. SHA-256 hash of verifier
2. Base64URL encode without padding
3. Method: S256 (required by OAuth 2.1)
If you discover a security vulnerability, please email security@epistates.com or open a confidential GitHub Security Advisory. Do not open public issues for security vulnerabilities.
All security-critical dependencies are industry-standard and regularly audited:
| Crate | Purpose | Security Track Record |
|---|---|---|
ring |
Cryptography (CSPRNG, PKCE) | Used by Google, Chromium, Firefox |
rustls |
TLS implementation | Memory-safe, no OpenSSL CVEs |
chacha20poly1305 |
Encryption | NCC Group audit (2020) |
keyring |
OS credential storage | 1M+ downloads, cross-platform |
secrecy |
Secret handling | De facto Rust standard |
zeroize |
Memory clearing | Prevents secret leakage |
No known vulnerabilities in any security-critical dependency.
Add to your Cargo.toml:
[dependencies]
schwab-rs = { version = "0.1", features = ["callback-server"] }
schwab-types = "0.1"use schwab_rs::{SchwabClient, SchwabConfig, auth::{OAuthConfig, TokenNotification}};
use std::sync::Arc;
// Configure OAuth with notifications
let oauth_config = OAuthConfig {
app_key: "YOUR_32_CHAR_APP_KEY".to_string(),
app_secret: "YOUR_16_CHAR_SECRET".to_string(),
callback_url: "https://127.0.0.1:8080".to_string(),
capture_callback: true, // Auto-capture auth code
auto_refresh: true, // Auto-refresh tokens
refresh_buffer_seconds: 61, // Refresh before expiry
on_token_notification: Some(Arc::new(|notification| {
if let TokenNotification::RefreshTokenExpiring { hours_remaining } = notification {
println!("Warning: Refresh token expires in {} hours", hours_remaining);
}
})),
..Default::default()
};
// Create client
let config = SchwabConfig { oauth: oauth_config, ..Default::default() };
let client = SchwabClient::new(config)?;
// Start background token management
client.init().await?;use schwab_rs::auth::AuthManager;
// For initial OAuth, create an AuthManager and run the flow once
let auth = AuthManager::new(client_config.oauth.clone())?;
let (auth_url, _code) = auth.authorize().await?;
println!("Open this URL in a browser: {}", auth_url);
// After authorization, tokens are saved to the configured file and auto-refreshed by client.init()// Get quotes
let quotes = client.get_quotes(&["AAPL", "MSFT"]).await?;
// Advanced quotes: only request specific fields and enable indicative pricing
let adv_quotes = client.get_quotes_with_options(
&["AAPL"],
Some("quote,reference"),
Some(true)
).await?;
// Get option chain
let chain = client.get_option_chain("SPY").await?;
// Preview an order (validate without executing)
let preview = client.preview_order("account_hash", &order).await?;
// Place an order
let response = client.place_order("account_hash", &order).await?;use schwab_rs::streaming::{StreamClient, StreamMessage};
use schwab_rs::types::streaming::StreamService;
// Build streaming client
let stream_client = StreamClient::builder()
.config(stream_config)
.auth_manager(auth_manager.clone())
.customer_id(customer_id)
.build()?;
// Connect and (attempt to) subscribe
stream_client.connect().await?;
stream_client.subscribe(StreamService::LeveloneEquities, vec!["AAPL".into(), "MSFT".into()]).await?;
// Handle messages
if let Some(mut receiver) = stream_client.get_receiver() {
while let Some(msg) = receiver.recv().await {
match msg {
StreamMessage::Data(data) => println!("Market data: {:?}", data),
StreamMessage::Response(resp) => println!("Response: {:?}", resp),
StreamMessage::Notify(hb) => println!("Heartbeat: {}", hb.heartbeat),
}
}
}Note: Streaming authentication, reconnection, and subscription handling are fully implemented.
For development, you need an HTTPS callback URL. Options:
# Install cloudflared
brew install cloudflared # macOS
# Create tunnel to local port 8080
cloudflared tunnel --url http://localhost:8080
# Use the generated HTTPS URL as your callback_urlngrok http 8080
# Use the HTTPS URL provided# Generate certificate
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
# Configure in your app to use HTTPS- Automatically refreshed before expiry
- Background task handles refresh seamlessly
- Must restart OAuth flow when expired
- SDK detects expiration and returns appropriate error
- Optional notifications warn before expiry
# Run the auth test example
cd examples/auth_test
cargo run -- --app-key YOUR_KEY --app-secret YOUR_SECRET statusexport SCHWAB_APP_KEY="your_32_character_app_key"
export SCHWAB_APP_SECRET="your_16_char_secret"
export SCHWAB_CALLBACK_URL="https://your-callback-url"
export SCHWAB_TOKENS_FILE="schwab_tokens.json"See the examples/ directory for working examples:
auth_test/- OAuth flow testing and token managementoauth_flow/- Minimal end-to-end OAuthoauth_callback_server/- Local callback capturecomprehensive/- Combined flows and API usage
callback-server: runs a local Axum server to capture OAuth callback
The SDK provides three secure token storage options (automatically selected by platform):
-
File (Basic security)
- Plain JSON with 0600 permissions (owner read/write only)
- Useful for quick migration from older versions
-
EncryptedFile (High security) ⭐ Default on Linux/Windows
- ChaCha20Poly1305 authenticated encryption (NCC Group audited)
- Tamper detection via AEAD authentication tag
- Random key generation with secure storage
- 0600 permissions on both token and key files
-
Keychain (Best security) ⭐ Default on macOS
- OS-native secure credential storage
- macOS: Keychain, Windows: Credential Manager, Linux: Secret Service
- Protected by user authentication
- Cross-platform via
keyringcrate
use schwab_rs::{SchwabClient, SchwabConfig};
use schwab_rs::auth::{OAuthConfig, TokenStoreKind};
let oauth = OAuthConfig {
app_key: "YOUR_APP_KEY".into(),
app_secret: "YOUR_SECRET".into(),
callback_url: "https://127.0.0.1:8080".into(),
// Choose storage backend (defaults to Keychain on macOS, EncryptedFile elsewhere)
token_store_kind: TokenStoreKind::Keychain, // OS-native (best)
// token_store_kind: TokenStoreKind::EncryptedFile, // Encrypted file (recommended)
// token_store_kind: TokenStoreKind::File, // Plain file (basic)
..Default::default()
};
let cfg = SchwabConfig { oauth, ..Default::default() };
let client = SchwabClient::new(cfg)?;
client.init().await?; // Tokens automatically use selected storageSecurity Features:
- Memory safety: Tokens use
SecretStringwith automatic zeroing on drop - Encryption: ChaCha20Poly1305 AEAD (EncryptedFile backend)
- File permissions: 0600 enforcement on Unix systems
- Keychain support: Cross-platform OS-native storage
- No logging: Tokens never appear in debug/log output
The SDK provides sensible defaults that can be customized:
use schwab_rs::config::{SchwabConfig, ClientConfig, RateLimitConfig, RetryConfig};
use std::time::Duration;
let config = SchwabConfig {
client: ClientConfig {
timeout: Duration::from_secs(10),
rate_limit: RateLimitConfig {
enabled: true,
requests_per_second: 120,
burst_size: 20,
},
retry: RetryConfig {
max_retries: 3,
initial_backoff: Duration::from_secs(1),
max_backoff: Duration::from_secs(30),
backoff_multiplier: 2.0,
retry_on_status: vec![429, 500, 502, 503, 504],
},
..Default::default()
},
..Default::default()
};The SDK provides comprehensive error types for robust error handling:
use schwab_rs::error::Error;
match client.get_quotes(&["AAPL"]).await {
Ok(quotes) => println!("Success: {:?}", quotes),
Err(Error::Auth(e)) => println!("Authentication error: {}", e),
Err(Error::RateLimit { retry_after }) => println!("Rate limit exceeded, retry after {}s", retry_after),
Err(Error::Network(e)) => println!("Network error: {}", e),
Err(e) => println!("Other error: {}", e),
}- Exact Callback URL Match: Must exactly match registered URL (HTTPS only, no trailing slash)
- Key Lengths: App key must be 32 chars, secret must be 16 chars
- Rate Limits: SDK defaults to 120 requests/second with burst of 20
- Token Expiration: Access tokens expire in ~30 minutes; refresh tokens in ~7 days
- Session Management: HTTP client is recreated after token refresh
- Token Storage: Defaults to Keychain (macOS) or EncryptedFile (other platforms)
- Token Notifications: Optional callbacks for expiry warnings and session events
Known limitations and notes:
- Endpoints under
crates/schwab-rs/src/endpoints/now delegate toSchwabClientmethods (thin wrappers) - Optional PKCE can be enabled via
OAuthConfig.pkce_enabled SchwabClientusestransport::http::HttpTransportfor REST
Control memory usage with bounded channels:
use schwab_rs::{StreamConfig, ChannelKind};
let mut config = StreamConfig::default();
// Unbounded (default - no backpressure)
config.channel_kind = ChannelKind::Unbounded;
// Bounded (recommended for production - prevents memory growth)
config.channel_kind = ChannelKind::Bounded(10000); // 10k message buffer
let stream = StreamClient::builder()
.config(config)
.auth_manager(auth_manager)
.customer_id(customer_id)
.build()?;Request only needed fields for bandwidth optimization:
use schwab_rs::types::streaming::StreamService;
// Request lean field set (Symbol, Bid, Ask, Last, Volume)
stream.set_service_fields(
StreamService::LeveloneEquities,
"0,1,2,3,8".to_string()
);
stream.subscribe_level_one_equities(&["AAPL", "MSFT"]).await?;Automatic Ping/Pong heartbeat with timeout detection (v0.2.0):
let mut config = StreamConfig::default();
config.heartbeat_interval = Duration::from_secs(20); // Ping every 20s
config.ping_timeout = Duration::from_secs(30); // Reconnect if no Pong
// Automatic timeout detection and reconnection!See examples/ directory for production-ready patterns:
streaming_demo.rs- All 13 services demonstratedstreaming_data_processing.rs- Async processing patternstreaming_quotes.rs- Real-time quotes dashboard
cargo run --example streaming_quotes --features callback-serverContributions are welcome! Please see CONTRIBUTING.md for guidelines.
This project is dual-licensed under the MIT License and Apache License 2.0.
This SDK is not affiliated with, endorsed by, or officially connected with Charles Schwab & Co., Inc. Use at your own risk. Always test thoroughly with sandbox data before using in production.
For issues and questions:
- GitHub Issues: Project Issues
- Documentation: /docs
Set required environment variables (or place in .env):
export SCHWAB_APP_KEY=...
export SCHWAB_APP_SECRET=...
export SCHWAB_CALLBACK_URL=https://127.0.0.1:8080Optional:
export SCHWAB_PKCE_ENABLED=false
export SCHWAB_TOKEN_STORE=file # Options: file, encrypted_file, keychain# Basic OAuth flow
cargo run -p oauth-flow
# Comprehensive SDK demonstration
cargo run -p comprehensive
# Authenticate and test token management
cargo run -p auth_test -- --app-key KEY --app-secret SECRET status
# Streaming demos
cargo run -p streaming-examples --bin streaming_demo
cargo run -p streaming-examples --bin streaming_quotes
cargo run -p streaming-examples --bin streaming_processing