Errors & retry
Every provider-facing endpoint shares the same error shapes and retry contract. ProviderClient already implements the "what's retried" column below for every call it makes — this page is the full reference for when you're debugging a thrown error, implementing a call the SDK doesn't cover, or writing your own retry logic around isRetryableStatus.
Status code reference
| Status | Meaning | Body | SDK behavior |
|---|---|---|---|
200 with status: "DECLINED" | Normal business outcome for a BET (insufficient funds, outside bet limits) | JSON TransactionResponse | Returned normally — not an error, never retried |
400 | Invalid request body (validation failure), or the request doesn't match the session it references (playerRef/gameId/currency mismatch) | JSON { "error": "..." } | Thrown as PlatformApiError, never retried |
400 (nonce) | Missing or oversized X-Nonce | JSON { "error": "invalid nonce" } | Thrown as PlatformApiError, never retried |
401 (signature) | Bad, missing, or expired-timestamp signature | Plain text: invalid signature | Thrown as PlatformApiError, never retried |
401 (replay) | A nonce that's already been used | JSON { "error": "replayed request" } | Thrown as PlatformApiError, never retried |
401 (identity) | No provider identity resolved, or the session token is invalid/expired | JSON { "error": "..." } | Thrown as PlatformApiError, never retried |
403 | The session belongs to a different provider than the one that signed this request | JSON { "error": "..." } | Thrown as PlatformApiError, never retried |
404 | No matching route — check baseUrl and the path | JSON { "error": "..." } | Thrown as PlatformApiError, never retried |
409 | Another attempt for this transactionId is already in flight | JSON { "error": "..." } | Retried with backoff, same transactionId |
429 | Rate limit exceeded (per-tenant) | JSON { "error": "rate limit exceeded" }, Retry-After header (seconds) | Thrown as PlatformApiError, not retried automatically — honor retryAfter yourself |
500 | Retryable platform-side failure (includes a TIMED_OUT resolution) | JSON { "error": "..." } | Retried with backoff |
503 | Game or operator adapter not configured | JSON { "error": "..." } | Retried with backoff — see the note below |
Every error response other than the plain-text 401 above shares the body shape { "error": "<message>" }.
A 503 is retried even though it's usually not transient
isRetryableStatus treats any status >= 500 as worth retrying, including 503. In practice a 503 from this API almost always means a game/operator configuration problem on the platform side (an adapter that hasn't been wired up), not a momentary blip — so a submitTransaction call that exhausts all its retries on repeated 503s is a signal to check your integration's configuration with the platform team, not to assume a network issue.
PlatformApiError
Thrown for every non-retryable response above (400/401/403/404/429) and for a 409/5xx that's exhausted all retries:
class PlatformApiError extends Error {
status: number
body: string // the raw response body
requestId?: string // mirrors the platform's X-Request-Id header
retryAfter?: string // mirrors the Retry-After header, when present
hint?: string // a short pointer at the likely cause
}requestId is the single most useful thing to hand platform support — it lets them find the exact request in server-side logs without you needing to describe timing or payload. hint is a one-line pointer at the likely cause for that specific status, not an exhaustive diagnosis. See Debugging for a worked example.
RequestTimeoutError
Thrown when a single request attempt exceeds timeoutMs (default 15000). It's a network-level failure, not a PlatformApiError — no HTTP response was ever received — so it's retried like any other network error. It only reaches a caller once every attempt across all retries has timed out.
class RequestTimeoutError extends Error {
timeoutMs: number
hint: string
}isRetryableStatus
function isRetryableStatus(status: number): boolean
// true for 409 and any status >= 500
// false for everything else, including 429Exported for callers building custom retry logic around a call the SDK doesn't cover. This is exactly the predicate ProviderClient uses internally — a network error (DNS failure, connection refused, etc.) is always treated as retryable regardless of this function, since it never produced an HTTP status to check.
Idempotency and retries
Retries always reuse the exact same transactionId (for wallet calls) — this is what lets the platform tell "the same logical attempt, retried" apart from "a new attempt." You never need to implement this yourself for calls made through ProviderClient; if you're signing a request by hand, reuse the same ID across your own retries the same way.
Free-spins webhooks (which the platform calls, and does not retry) use a different idempotency anchor — requestRef/externalRef — since the platform is the one that would need to retry there, and it doesn't. See Free spins webhook for that contract.