# 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`. The examples below use the OAuth 2.0 Code Flow.

### Basic Setup

```typescript
import { createSchwabAuth, createApiClient } from '@sudowealth/schwab-api'

// Create the auth manager (Enhanced OAuth client)
const auth = createSchwabAuth({
	oauthConfig: {
		clientId: process.env.SCHWAB_CLIENT_ID!,
		clientSecret: process.env.SCHWAB_CLIENT_SECRET!,
		redirectUri: 'https://example.com/callback',
		// Optional: provide persistence callbacks
		// save: async (tokens) => { ... },
		// load: async () => { ... },
	},
})

// Generate login URL
console.log('Visit:', auth.getAuthorizationUrl().authUrl)

// Exchange auth code for tokens (when user returns)
const tokens = await auth.exchangeCode('<authorization-code>')

// Optionally persist tokens here...

// Create API client using the auth manager
const schwab = createApiClient({ auth })
```

### Market Data

```typescript
// Get real-time quotes
const quotes = await schwab.marketData.quotes.getQuotes({
	queryParams: {
		symbols: 'AAPL,MSFT,GOOGL',
		fields: 'quote,fundamental',
	},
})

// Get price history
const history = await schwab.marketData.priceHistory.getPriceHistory({
	queryParams: {
		symbol: 'AAPL',
		periodType: 'day',
		period: 10,
		frequencyType: 'minute',
		frequency: 1,
	},
})

// Get options chain
const options = await schwab.marketData.options.getOptionChain({
	queryParams: {
		symbol: 'AAPL',
		contractType: 'CALL',
		strikeCount: 10,
	},
})

// Get market hours
const hours = await schwab.marketData.marketHours.getMarketHours({
	queryParams: {
		markets: ['equity', 'option'],
	},
})

// Get movers
const movers = await schwab.marketData.movers.getMovers({
	pathParams: { symbol_id: '$SPX' },
	queryParams: { sort: 'up', frequency: 0 },
})

// Search instruments
const instruments = await schwab.marketData.instruments.getInstruments({
	queryParams: {
		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.getAccountByNumber({
	pathParams: { accountNumber: 'your-account-hash' },
	queryParams: { fields: 'positions' },
})

// Get orders
const orders = await schwab.trader.orders.getOrdersByAccount({
	pathParams: { accountNumber: 'your-account-hash' },
	queryParams: { maxResults: 50 },
})

// Place an order
const orderResponse = await schwab.trader.orders.placeOrderForAccount({
	pathParams: { accountNumber: 'your-account-hash' },
	body: {
		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({
	pathParams: { accountNumber: 'your-account-hash' },
	queryParams: {
		types: '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 { createSchwabAuth, createApiClient } from '@sudowealth/schwab-api'

const auth = createSchwabAuth({
	oauthConfig: {
		clientId: process.env.SCHWAB_CLIENT_ID!,
		clientSecret: process.env.SCHWAB_CLIENT_SECRET!,
		redirectUri: 'https://example.com/callback',
	},
})

// Customize middleware via options
const schwab = createApiClient({
	auth,
	middleware: {
		rateLimit: { maxRequests: 120, windowMs: 60_000 },
		retry: { maxAttempts: 3, baseDelayMs: 1000 },
	},
})
```

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