2.1.0 • Published 1 month ago

@sudowealth/schwab-api v2.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
1 month ago

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
  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:

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

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

// 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

// 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

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

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.

// ❌ 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
# .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

    // ❌ 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

    // ❌ NEVER hardcode secrets
    const clientSecret = 'abc123-secret-key'
    
    // ✅ Use environment variables
    const clientSecret = process.env.SCHWAB_CLIENT_SECRET
  3. Exposing Error Details

    // ❌ 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

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:

npm install @sudowealth/schwab-api@beta

License

MIT

2.1.0

1 month ago

2.1.0-beta.26

1 month ago

2.1.0-beta.25

1 month ago

2.1.0-beta.24

1 month ago

2.1.0-beta.23

1 month ago

2.1.0-beta.22

1 month ago

2.1.0-beta.21

1 month ago

2.1.0-beta.20

1 month ago

2.1.0-beta.19

1 month ago

2.1.0-beta.18

1 month ago

2.1.0-beta.17

1 month ago

2.1.0-beta.16

1 month ago

2.1.0-beta.15

1 month ago

2.1.0-beta.14

1 month ago

2.1.0-beta.13

1 month ago

2.1.0-beta.12

1 month ago

2.1.0-beta.11

1 month ago

2.1.0-beta.10

1 month ago

2.1.0-beta.9

1 month ago

2.1.0-beta.8

1 month ago

2.1.0-beta.7

1 month ago

2.1.0-beta.6

2 months ago

2.1.0-beta.5

2 months ago

2.1.0-beta.4

2 months ago

2.1.0-beta.3

2 months ago

2.1.0-beta.2

2 months ago

2.1.0-beta.1

2 months ago

2.0.2

2 months ago

2.0.1

2 months ago

2.0.0

2 months ago

1.2.0-beta.96

2 months ago

1.2.0-beta.95

2 months ago

1.2.0-beta.94

2 months ago

1.2.0-beta.93

2 months ago

1.2.0-beta.92

2 months ago

1.2.0-beta.91

2 months ago

1.2.0-beta.90

2 months ago

1.2.0-beta.89

2 months ago

1.2.0-beta.88

2 months ago

1.2.0-beta.87

2 months ago

1.2.0-beta.86

2 months ago

1.2.0-beta.85

2 months ago

1.2.0-beta.84

2 months ago

1.2.0-beta.83

2 months ago

1.2.0-beta.82

2 months ago

1.2.0-beta.81

2 months ago

1.2.0-beta.80

2 months ago

1.2.0-beta.79

2 months ago

1.2.0-beta.78

2 months ago

1.2.0-beta.77

2 months ago

1.2.0-beta.76

2 months ago

1.2.0-beta.75

2 months ago

1.2.0-beta.74

2 months ago

1.2.0-beta.73

2 months ago

1.2.0-beta.72

2 months ago

1.2.0-beta.71

2 months ago

1.2.0-beta.70

2 months ago

1.2.0-beta.69

2 months ago

1.2.0-beta.68

2 months ago

1.2.0-beta.67

2 months ago

1.2.0-beta.66

2 months ago

1.2.0-beta.65

2 months ago

1.2.0-beta.64

2 months ago

1.2.0-beta.63

2 months ago

1.2.0-beta.62

2 months ago

1.2.0-beta.61

2 months ago

1.2.0-beta.60

2 months ago

1.2.0-beta.59

2 months ago

1.2.0-beta.58

2 months ago

1.2.0-beta.57

2 months ago

1.2.0-beta.56

2 months ago

1.2.0-beta.55

2 months ago

1.2.0-beta.54

2 months ago

1.2.0-beta.53

2 months ago

1.2.0-beta.52

2 months ago

1.2.0-beta.51

2 months ago

1.2.0-beta.50

2 months ago

1.2.0-beta.49

2 months ago

1.2.0-beta.48

2 months ago

1.2.0-beta.47

2 months ago

1.2.0-beta.46

2 months ago

1.2.0-beta.45

2 months ago

1.2.0-beta.44

2 months ago

1.2.0-beta.43

2 months ago

1.2.0-beta.42

2 months ago

1.2.0-beta.41

2 months ago

1.2.0-beta.40

2 months ago

1.2.0-beta.39

2 months ago

1.2.0-beta.38

2 months ago

1.2.0-beta.37

2 months ago

1.2.0-beta.36

2 months ago

1.2.0-beta.35

2 months ago

1.2.0-beta.34

2 months ago

1.2.0-beta.33

2 months ago

1.2.0-beta.32

2 months ago

1.2.0-beta.31

2 months ago

1.2.0-beta.30

2 months ago

1.2.0-beta.29

2 months ago

1.2.0-beta.28

2 months ago

1.2.0-beta.27

2 months ago

1.2.0-beta.26

2 months ago

1.2.0-beta.25

2 months ago

1.2.0-beta.24

2 months ago

1.2.0-beta.23

2 months ago

1.2.0-beta.22

2 months ago

1.2.0-beta.21

2 months ago

1.2.0-beta.20

2 months ago

1.2.0-beta.19

2 months ago

1.2.0-beta.18

2 months ago

1.2.0-beta.17

2 months ago

1.2.0-beta.16

2 months ago

1.2.0-beta.15

2 months ago

1.2.0-beta.14

2 months ago

1.2.0-beta.13

2 months ago

1.2.0-beta.12

2 months ago

1.2.0-beta.11

2 months ago

1.2.0-beta.10

2 months ago

1.2.0-beta.9

2 months ago

1.2.0-beta.8

2 months ago

1.2.0-beta.7

2 months ago

1.2.0-beta.6

2 months ago

1.2.0-beta.5

2 months ago

1.2.0-beta.4

2 months ago

1.2.0-beta.3

2 months ago

1.2.0-beta.2

2 months ago

1.2.0-beta.1

2 months ago

1.1.0

2 months ago

1.0.11

2 months ago

1.0.10

2 months ago

1.0.9

2 months ago

1.0.8

2 months ago

1.0.7

2 months ago

1.0.6

2 months ago

1.0.5

2 months ago

1.0.4

2 months ago

1.0.3

2 months ago

1.0.2

2 months ago

1.0.1

2 months ago

1.0.0

2 months ago

0.0.8

2 months ago

0.0.7

2 months ago

0.0.6

2 months ago

0.0.5

2 months ago

0.0.4

2 months ago

0.0.3

2 months ago

0.0.2

2 months ago

0.0.1

2 months ago