SchwabApi.Client
1.0.0-beta.1
dotnet add package SchwabApi.Client --version 1.0.0-beta.1
NuGet\Install-Package SchwabApi.Client -Version 1.0.0-beta.1
<PackageReference Include="SchwabApi.Client" Version="1.0.0-beta.1" />
<PackageVersion Include="SchwabApi.Client" Version="1.0.0-beta.1" />
<PackageReference Include="SchwabApi.Client" />
paket add SchwabApi.Client --version 1.0.0-beta.1
#r "nuget: SchwabApi.Client, 1.0.0-beta.1"
#:package SchwabApi.Client@1.0.0-beta.1
#addin nuget:?package=SchwabApi.Client&version=1.0.0-beta.1&prerelease
#tool nuget:?package=SchwabApi.Client&version=1.0.0-beta.1&prerelease
SchwabApi.Client
HTTP client library for making authenticated requests to the Schwab Trading API with automatic retry, circuit breaker, and comprehensive error handling.
Installation
dotnet add package SchwabApi.Client
Or via NuGet Package Manager:
Install-Package SchwabApi.Client
Features
- HTTP Methods: GET, POST, PUT, PATCH, DELETE with typed request/response models
- OAuth Integration: Automatic Bearer token injection via
IOAuthService - Resilience Policies: Retry with exponential backoff, circuit breaker, timeout
- Error Mapping: HTTP status codes → typed exceptions with context
- Dependency Injection: ASP.NET Core-ready with
IOptions<SchwabApiOptions>configuration - Rate Limit Handling: Automatic parsing of
X-RateLimit-*headers - Thread-Safe: Designed for concurrent request scenarios
Installation
dotnet add package SchwabApi.Client
Quick Start
Important: Follow the Standard Credential Management Pattern to load credentials from environment variables, not hardcoded values. The examples below use placeholder strings for illustration only.
1. Register Services
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
// Load configuration securely
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.AddEnvironmentVariables()
.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
// Get credentials from environment variables (NEVER hardcode)
var appKey = Environment.GetEnvironmentVariable("SCHWAB_APP_KEY")
?? throw new InvalidOperationException("SCHWAB_APP_KEY not set");
var appSecret = Environment.GetEnvironmentVariable("SCHWAB_APP_SECRET")
?? throw new InvalidOperationException("SCHWAB_APP_SECRET not set");
var callbackUrl = configuration["Schwab:CallbackUrl"]
?? "https://127.0.0.1:5001/callback";
// Register OAuth service first
services.AddSchwabAuth(options =>
{
options.ClientId = appKey;
options.ClientSecret = appSecret;
options.RedirectUri = callbackUrl;
});
// Register HTTP client
services.AddSchwabApiClient(options =>
{
options.BaseUrl = "https://api.schwabapi.com";
options.AppKey = appKey;
options.AppSecret = appSecret;
options.RetryCount = 3;
options.TimeoutSeconds = 30;
});
var serviceProvider = services.BuildServiceProvider();
2. Make Authenticated Requests
using SchwabApi.Client;
var client = serviceProvider.GetRequiredService<ISchwabApiClient>();
// GET request
var accountsResponse = await client.GetAsync<AccountList>("/trader/v1/accounts");
Console.WriteLine($"Status: {accountsResponse.StatusCode}");
Console.WriteLine($"Data: {accountsResponse.Data}");
Console.WriteLine($"Rate Limit Remaining: {accountsResponse.RateLimitRemaining}");
// POST request
var order = new OrderRequest { Symbol = "AAPL", Quantity = 10, Side = "BUY" };
var orderResponse = await client.PostAsync<OrderRequest, OrderResponse>(
"/trader/v1/accounts/12345/orders",
order);
// Handle errors
try
{
var response = await client.GetAsync<Quote>("/marketdata/v1/quotes/INVALID");
}
catch (ResourceNotFoundException ex)
{
Console.WriteLine($"Not found: {ex.Message}");
Console.WriteLine($"URL: {ex.RequestUrl}");
}
catch (RateLimitException ex)
{
Console.WriteLine($"Rate limited. Retry after: {ex.RetryAfter}");
}
3. Configuration via appsettings.json
Note: Store only non-sensitive configuration in appsettings.json. Keep App Key and App Secret in environment variables per the Standard Credential Management Pattern.
{
"Schwab": {
"CallbackUrl": "https://127.0.0.1:5001/callback"
},
"SchwabApi": {
"BaseUrl": "https://api.schwabapi.com",
"TimeoutSeconds": 30,
"RetryCount": 3,
"RetryBaseDelaySeconds": 2,
"CircuitBreakerFailureThreshold": 5,
"CircuitBreakerCooldownSeconds": 30
}
}
Then set environment variables for sensitive credentials:
PowerShell:
$env:SCHWAB_APP_KEY = "your_app_key_from_schwab_portal"
$env:SCHWAB_APP_SECRET = "your_app_secret_from_schwab_portal"
Linux/macOS:
export SCHWAB_APP_KEY="your_app_key_from_schwab_portal"
export SCHWAB_APP_SECRET="your_app_secret_from_schwab_portal"
services.AddSchwabApiClient(configuration.GetSection("SchwabApi"));
Configuration Options
| Property | Type | Default | Description |
|---|---|---|---|
BaseUrl |
string | https://api.schwabapi.com |
Schwab API base URL (must be HTTPS) |
AppKey |
string | (required) | OAuth client ID |
AppSecret |
string | (required) | OAuth client secret |
TimeoutSeconds |
int | 30 | HTTP request timeout (1-300 seconds) |
RetryCount |
int? | 3 | Number of retry attempts for transient failures (0-10) |
RetryBaseDelaySeconds |
int? | 2 | Base delay for exponential backoff (1-10 seconds) |
CircuitBreakerFailureThreshold |
int? | 5 | Failures before circuit opens (1-20) |
CircuitBreakerCooldownSeconds |
int? | 30 | Cooldown before half-open state (10-300 seconds) |
Resilience Policies
Retry Policy
- Transient Errors: HTTP 500, 502, 503, 504, 408, and timeout exceptions
- Exponential Backoff:
delay = baseDelay^retryAttempt(e.g., 2s, 4s, 8s for 3 retries) - No Retry: Client errors (400, 401, 403, 404) are not retried
Circuit Breaker
- Failure Threshold: Opens circuit after N consecutive failures (default: 5)
- Cooldown: Remains open for N seconds before testing recovery (default: 30s)
- Half-Open State: Allows one test request before fully closing
Timeout Policy
- Per-Request: Applies to each individual HTTP request
- Pessimistic: Cancels request immediately when timeout expires
Error Handling
The client maps HTTP status codes to typed exceptions:
| Status Code | Exception | Description |
|---|---|---|
| 400 | ValidationException |
Invalid request parameters |
| 401 | AuthenticationException |
Missing or invalid access token |
| 403 | AuthorizationException |
Insufficient permissions |
| 404 | ResourceNotFoundException |
Endpoint or resource not found |
| 429 | RateLimitException |
Rate limit exceeded, includes RetryAfter |
| 500, 502, 503, 504 | ServerException |
Schwab API server error |
| Timeout | TimeoutException |
Request exceeded configured timeout |
All exceptions include:
StatusCode: HTTP status codeRequestUrl: Full request URLCorrelationId: Schwab's correlation ID (if available)ResponseBody: Raw response for debugging
Rate Limits
The client automatically parses Schwab's rate limit headers:
var response = await client.GetAsync<Quote>("/marketdata/v1/quotes/AAPL");
Console.WriteLine($"Limit: {response.RateLimitLimit}");
Console.WriteLine($"Remaining: {response.RateLimitRemaining}");
Console.WriteLine($"Reset: {response.RateLimitReset}"); // Unix timestamp
Thread Safety
ISchwabApiClient is registered as a singleton and is thread-safe. It can handle multiple concurrent requests:
var client = serviceProvider.GetRequiredService<ISchwabApiClient>();
var tasks = Enumerable.Range(0, 100).Select(i =>
client.GetAsync<Quote>($"/marketdata/v1/quotes/SYMBOL{i}"));
var responses = await Task.WhenAll(tasks);
Advanced Scenarios
Custom HttpClient Configuration
services.AddHttpClient("SchwabApiClient", client =>
{
client.DefaultRequestHeaders.Add("X-Custom-Header", "value");
client.Timeout = TimeSpan.FromSeconds(60);
});
services.AddSchwabApiClient(options => { /* ... */ });
Logging
The client uses ILogger<SchwabApiClient> for structured logging:
- Retry Attempts: Logs each retry with delay and reason
- Circuit Breaker: Logs state changes (OPEN, CLOSED, HALF-OPEN)
- Timeout Warnings: Logs when requests approach timeout threshold
Enable logging in appsettings.json:
{
"Logging": {
"LogLevel": {
"SchwabApi.Client": "Information",
"SchwabApi.Client.Resilience": "Warning"
}
}
}
Correlation IDs
Schwab includes a correlation ID in responses for debugging:
try
{
var response = await client.GetAsync<Quote>("/marketdata/v1/quotes/AAPL");
}
catch (SchwabApiException ex)
{
Console.WriteLine($"Correlation ID: {ex.CorrelationId}");
// Provide this to Schwab support for troubleshooting
}
Testing
Mock ISchwabApiClient in your tests:
var mockClient = new Mock<ISchwabApiClient>();
mockClient
.Setup(x => x.GetAsync<AccountList>("/trader/v1/accounts", It.IsAny<CancellationToken>()))
.ReturnsAsync(new ApiResponse<AccountList>
{
Data = new AccountList { Accounts = new[] { /* ... */ } },
StatusCode = HttpStatusCode.OK,
Headers = new Dictionary<string, string>()
});
// Inject mock into your service
var yourService = new YourService(mockClient.Object);
Related Packages
- SchwabApi.Authentication: OAuth 2.0 authorization code flow with PKCE
- SchwabApi.MarketData: Typed models for market data endpoints
- SchwabApi.Trading: Typed models for trading endpoints
License
Licensed under the MIT License. See LICENSE in the project root for license information.
Support
- Issues: GitHub Issues
- Documentation: Schwab API Documentation
- Examples: See
examples/directory for complete working examples
Changelog
Version 0.2.0
- Initial HTTP client implementation
- OAuth Bearer token injection
- Polly resilience policies (retry, circuit breaker, timeout)
- Comprehensive error mapping
- Rate limit header parsing
- Dependency injection support
- Full test coverage (47 tests)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.Http (>= 10.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Options (>= 10.0.1)
- Polly (>= 8.6.5)
- SchwabApi.Authentication (>= 1.0.0-beta.1)
- SchwabApi.Models (>= 1.0.0-beta.1)
- System.Text.Json (>= 10.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|