Errors & retries
Every public service method throws a typed exception for the error conditions you care about. Network blips are auto-retried via Polly before surfacing.
Exceptions you’ll see
Section titled “Exceptions you’ll see”| Exception | When | Recovery |
|---|---|---|
STXWrongCredentialsException |
Email or password rejected at login. Legacy path only | Prompt for fresh credentials |
STXSessionExpiredException |
JWT and refresh token both expired. Legacy path only | Re-authenticate |
STXTokenExpiredException |
JWT expired but refresh token is still valid | Call STXTokenService.RefreshTokenAsync, or rely on keepSessionAlive: true |
STXCancelOnDisconnectNotEnabledException |
ConfirmOrderAsync(cancelOnDisconnect: true) called before joining STXActiveOrdersChannel |
Join the channel first, or pass cancelOnDisconnect: false |
STXGeoComplyException |
Geo-compliance check failed | User is outside a permitted jurisdiction; surface to your UI |
GraphQLHttpRequestException |
Non-200 HTTP response from the GraphQL endpoint | Usually transient; retries handled internally (see below) |
All STX-prefixed exceptions live in STX.Sdk.Exceptions.
Example: handle auth failures
Section titled “Example: handle auth failures”try{ var me = await identity.GetMeAsync();}catch (STXRequestFailedException ex){ // A rejected signature lands here. The usual cause is clock skew: the server // allows 30 seconds, so check NTP before suspecting the key. _logger.LogError(ex, "Could not authenticate");}catch (STXGeoComplyException ex){ // User-facing: "Trading isn't available in your region." _logger.LogWarning(ex, "Geo block");}Example: refresh on expired token (legacy path)
Section titled “Example: refresh on expired token (legacy path)”Only relevant to email and password. API keys have no token to expire, so none of this is needed.
When not using keepSessionAlive: true, wrap trading calls with a token-refresh retry:
async Task<T> WithSession<T>(Func<Task<T>> call){ try { return await call(); } catch (STXTokenExpiredException) { await _tokens.RefreshTokenAsync(); return await call(); } catch (STXSessionExpiredException) { await _login.LoginAsync(_email, _password); return await call(); }}
var order = await WithSession(() => _orders.ConfirmOrderAsync(price, qty, marketId, action, type));Transient retries (automatic)
Section titled “Transient retries (automatic)”Retry is applied per call site, not by the transport, so it covers some methods and not others. As of 1.6.0 it covers every order mutation, all of STXMarketService, STXIdentityService, terms-and-conditions acceptance, and login and token refresh, which previously had no retry at all.
These still call the GraphQL client directly and are not retried:
| Not retried |
|---|
STXOrderService.GetMyOrdersAsync |
STXTradeService.GetMyTradesAsync, GetMyTradesForOrderAsync |
STXSettlementService.GetMySettlementsAsync |
STXEventService.GetEventInfosAsync |
STXTermsAndConditionsService.GetTermsAndConditionsAsync |
STXGeoFencingLicenseService |
If a brief outage during one of those matters to you, wrap it yourself. Everywhere else, don’t add your own retry on top: double-retry amplifies an outage rather than absorbing it.
Where it does apply the policy is RequestNumberOfRetry = 3, meaning 3 retries after the initial call, so 4 requests in total, with exponential backoff plus jitter: roughly 200ms, 400ms and 800ms, so about 1.4s of sleep on top of the per-attempt HTTP timeout. Size any outer deadline or circuit breaker against that, not against a single request.
The policy retries anything that could plausibly succeed on a second attempt:
| Retried | Not retried |
|---|---|
HttpRequestException and other network failures |
STXWrongCredentialsException |
| 5xx from the GraphQL endpoint | STXUnauthorizedException, STXSessionExpiredException, STXTokenExpiredException |
| 404, which is what an ingress returns mid-deployment | STXBadQueryObjectException, STXBadArgumentException, ArgumentException |
Timeouts (TaskCanceledException wrapping TimeoutException) |
429, and genuine cancellation |
Credential errors are excluded deliberately, including a bare HTTP 401 or 403. Repeating a rejected credential cannot succeed, and on the legacy background re-login path it actively causes harm by driving the account towards a lockout.
If a failure persists past the built-in retries, STXRequestFailedException surfaces with the original exception as its InnerException, so the underlying cause (an HTTP status, a DNS failure, a timeout) is still available:
catch (STXRequestFailedException ex) when (ex.InnerException is GraphQLHttpRequestException http){ _logger.LogWarning("GraphQL returned {Status}", http.StatusCode);}Rate limits
Section titled “Rate limits”The exchange rate-limits by IP + account. Bursty order placement can return an HTTP 429. The SDK does not retry 429s (retrying is what the limiter is pushing back against). If you see them, lower your request rate or batch via ConfirmOrdersAsync.
Debugging
Section titled “Debugging”Enable Debug logging to see full GraphQL request/response bodies:
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug).AddConsole());Every call is logged with its operation name, variables, and elapsed time. That’s usually enough to tell whether an error was transient or a schema mismatch.

