@sudowealth/schwab-api
Version:
TypeScript client for Charles Schwab API with OAuth support, market data, trading functionality, and complete type safety
429 lines (338 loc) • 11.1 kB
Markdown
# Schwab API Client
TypeScript client for Charles Schwab API with OAuth support, market data,
trading functionality, and complete type safety.
## Unofficial Library
**This is an unofficial, community-developed TypeScript client library for
interacting with Schwab APIs. It has not been approved, endorsed, or certified
by Charles Schwab. It is provided as-is, and its functionality may be incomplete
or unstable. Use at your own risk, especially when dealing with financial data
or transactions.**
## Getting Started
To use the Schwab API, you'll need to register for a developer account:
1. Visit
[Schwab Developer Portal](https://developer.schwab.com/user-guides/get-started/introduction)
2. Sign up for a developer account
3. Create an application to obtain your client ID and secret
4. Review the API documentation and usage limits
## Features
- **OAuth Helper**: Client-credentials OAuth flow with automatic token handling
- **Request Pipeline**: Middleware system for auth, rate limits, and retries
- **Type Safety**: Complete TypeScript definitions for all API endpoints
- **Zod Validation**: Runtime schema validation for API responses
- **Market Data**: Real-time quotes, price history, options chains, market
hours, and movers
- **Trading**: Account management, order placement, transaction history, and
user preferences
## Installation
Available on [npm](https://www.npmjs.com/package/@sudowealth/schwab-api):
```bash
npm install @sudowealth/schwab-api
```
## Quick Start
> **Prerequisites**:
>
> 1. You must have a Schwab developer account. You can register at
> @https://developer.schwab.com/register.
> 2. Create an application at @https://developer.schwab.com/dashboard/apps.
> 3. In your application settings, provide your callback URL.
> 4. Obtain the Client ID (App Key) and Client Secret from your application
> page. These will be used as environment variables (e.g., `SCHWAB_CLIENT_ID`
> and `SCHWAB_CLIENT_SECRET`).
The quickest way to get started is by using `createSchwabAuth` along with
`createApiClient`. This example demonstrates using a static access token. For
more robust authentication, such as OAuth 2.0 Code Flow, refer to the "Usage
Examples" section.
### Basic Setup
```typescript
import {
createSchwabAuthClient,
configureSchwabApi,
} from '@sudowealth/schwab-api'
// Create unified auth client
const auth = createSchwabAuthClient({
clientId: process.env.SCHWAB_ID,
clientSecret: process.env.SCHWAB_SECRET,
redirectUri: 'https://example.com/callback',
})
// Generate login URL
console.log('Visit:', auth.getAuthorizationUrl().authUrl)
// Exchange auth code for tokens
const tokens = await auth.exchangeCode('<authorization-code>')
// Create API client
const schwab = await configureSchwabApi({
tokens: {
current: () => tokens,
refresh: () => auth.refresh(tokens.refresh_token),
},
})
```
### Market Data
```typescript
// Get real-time quotes
const quotes = await schwab.marketData.quotes.getQuotes({
symbols: 'AAPL,MSFT,GOOGL',
fields: 'quote,fundamental',
})
// Get price history
const history = await schwab.marketData.priceHistory.getPriceHistory({
symbol: 'AAPL',
periodType: 'day',
period: 10,
frequencyType: 'minute',
frequency: 1,
})
// Get options chain
const options = await schwab.marketData.options.getOptionChain({
symbol: 'AAPL',
contractType: 'CALL',
strikeCount: 10,
})
// Get market hours
const hours = await schwab.marketData.marketHours.getMarketHours({
markets: 'equity,option',
})
// Get movers
const movers = await schwab.marketData.movers.getMovers({
index: '$SPX.X',
direction: 'up',
change: 'percent',
})
// Search instruments
const instruments = await schwab.marketData.instruments.getInstruments({
symbol: 'AAPL',
projection: 'symbol-search',
})
```
### Trading
```typescript
// Get accounts
const accounts = await schwab.trader.accounts.getAccounts()
// Get account details
const account = await schwab.trader.accounts.getAccount({
accountId: 'your-account-hash',
fields: 'positions',
})
// Get orders
const orders = await schwab.trader.orders.getOrders({
accountId: 'your-account-hash',
maxResults: 50,
})
// Place an order
const orderResponse = await schwab.trader.orders.placeOrder({
accountId: 'your-account-hash',
orderType: 'MARKET',
session: 'NORMAL',
duration: 'DAY',
orderStrategyType: 'SINGLE',
orderLegCollection: [
{
instruction: 'BUY',
quantity: 10,
instrument: {
symbol: 'AAPL',
assetType: 'EQUITY',
},
},
],
})
// Get transactions
const transactions = await schwab.trader.transactions.getTransactions({
accountId: 'your-account-hash',
type: 'TRADE',
startDate: '2024-01-01',
endDate: '2024-12-31',
})
// Get user preferences
const preferences = await schwab.trader.userPreference.getUserPreference()
```
### Advanced Configuration with Middleware
```typescript
import {
configureSchwabApi,
withRateLimit,
withRetry,
} from '@sudowealth/schwab-api'
const schwab = await configureSchwabApi({
tokens: {
current: () => tokens,
refresh: () => auth.refresh(tokens.refresh_token),
},
middlewares: [
withRateLimit(120, 60000), // 120 requests per minute
withRetry({ max: 3, baseMs: 1000 }), // Retry with exponential backoff
],
})
```
## Important Notes
### Token Management
The auth client provides a unified interface for OAuth operations:
- **`getAuthorizationUrl()`**: Generate URL for user login
- **`exchangeCode(code)`**: Exchange authorization code for tokens
- **`refresh(refreshToken)`**: Refresh expired access tokens
### Refresh Token Expiration
**Important**: Schwab refresh tokens have a hard 7-day expiration limit that
cannot be extended. This is a security measure enforced by Schwab's API servers.
When a refresh token expires:
- The `refresh()` method will throw a `SchwabAuthError` with code
`TOKEN_EXPIRED`
- The user must complete a full re-authentication flow through Schwab's login
page
- There is no way to refresh tokens indefinitely without user interaction
#### Handling Token Expiration
```typescript
try {
const newTokens = await auth.refresh(oldRefreshToken)
// Update stored tokens
} catch (error) {
if (error instanceof SchwabAuthError && error.code === 'TOKEN_EXPIRED') {
// Redirect user to re-authenticate
const { authUrl } = auth.getAuthorizationUrl()
window.location.href = authUrl
}
}
```
### API Structure
The API client is organized into logical namespaces:
- **`marketData`**: Real-time and historical market data
- `quotes`: Real-time quotes and fundamentals
- `priceHistory`: Historical price data and charts
- `options`: Options chains and pricing
- `marketHours`: Trading hours for different markets
- `movers`: Top gaining/losing securities
- `instruments`: Security search and lookup
- **`trader`**: Account and trading operations
- `accounts`: Account information and positions
- `orders`: Order management and execution
- `transactions`: Transaction history and details
- `userPreference`: User settings and preferences
## Security Best Practices
### Token Storage
⚠️ **NEVER store tokens in plain text**. Always encrypt sensitive data before
storage.
```typescript
// ❌ BAD - Insecure plain text storage
const insecureStorage = {
save: async (tokens) => {
await fs.writeFile('tokens.json', JSON.stringify(tokens))
},
load: async () => {
const data = await fs.readFile('tokens.json', 'utf-8')
return JSON.parse(data)
},
}
// ✅ GOOD - Encrypted storage example
import crypto from 'crypto'
const secureStorage = {
save: async (tokens) => {
// Use a secure key management system in production
const key = process.env.ENCRYPTION_KEY
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(
'aes-256-gcm',
Buffer.from(key, 'hex'),
iv,
)
let encrypted = cipher.update(JSON.stringify(tokens), 'utf8', 'hex')
encrypted += cipher.final('hex')
const authTag = cipher.getAuthTag()
await secureStore.set('tokens', {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
})
},
load: async () => {
const data = await secureStore.get('tokens')
if (!data) return null
const key = process.env.ENCRYPTION_KEY
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(key, 'hex'),
Buffer.from(data.iv, 'hex'),
)
decipher.setAuthTag(Buffer.from(data.authTag, 'hex'))
let decrypted = decipher.update(data.encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return JSON.parse(decrypted)
},
}
```
### Credential Management
- **Never commit credentials**: Keep `.env` files in `.gitignore`
- **Use environment variables**: Store sensitive data in environment variables
or secure vaults
- **Rotate credentials regularly**: Implement a credential rotation policy
- **Principle of least privilege**: Only grant the minimum required permissions
```bash
# .env (never commit this file)
SCHWAB_CLIENT_ID=your-client-id
SCHWAB_CLIENT_SECRET=your-client-secret
ENCRYPTION_KEY=your-256-bit-hex-key
```
### Security Checklist
- [ ] Use HTTPS for all API communications
- [ ] Encrypt tokens before storing them
- [ ] Never log tokens or sensitive data
- [ ] Implement proper error handling that doesn't leak information
- [ ] Use secure key management (AWS KMS, Azure Key Vault, etc.)
- [ ] Monitor for suspicious activity
- [ ] Implement request signing if available
- [ ] Keep dependencies up to date
### Common Security Mistakes to Avoid
1. **Logging Sensitive Data**
```typescript
// ❌ NEVER log tokens
console.log('Access token:', tokens.access_token)
// ✅ Log only non-sensitive metadata
console.log('Token refreshed successfully')
```
2. **Storing Secrets in Code**
```typescript
// ❌ NEVER hardcode secrets
const clientSecret = 'abc123-secret-key'
// ✅ Use environment variables
const clientSecret = process.env.SCHWAB_CLIENT_SECRET
```
3. **Exposing Error Details**
```typescript
// ❌ Don't expose internal details
catch (error) {
res.json({ error: error.stack })
}
// ✅ Return generic error messages
catch (error) {
console.error('Internal error:', error) // Log internally
res.json({ error: 'Authentication failed' }) // Generic response
}
```
## Error Handling
```typescript
import { SchwabApiError, SchwabAuthError } from '@sudowealth/schwab-api'
try {
await schwab.trader.accounts.getAccounts()
} catch (error) {
if (error instanceof SchwabAuthError) {
if (error.code === 'TOKEN_EXPIRED') {
// Handle expired tokens
}
} else if (error instanceof SchwabApiError) {
// Handle API errors
console.error('API Error:', error.message)
}
}
```
## Development
- Clone the repository
- Install dependencies: `npm install`
- Build: `npm run build`
- Lint: `npm run lint`
- Type check: `npm run typecheck`
- Format: `npm run format`
- Validate all: `npm run validate`
### Installing Beta Versions
To install the latest beta release:
```bash
npm install @sudowealth/schwab-api@beta
```
## License
MIT