Skip to content

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

StatusMeaningBodySDK behavior
200 with status: "DECLINED"Normal business outcome for a BET (insufficient funds, outside bet limits)JSON TransactionResponseReturned normally — not an error, never retried
400Invalid 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-NonceJSON { "error": "invalid nonce" }Thrown as PlatformApiError, never retried
401 (signature)Bad, missing, or expired-timestamp signaturePlain text: invalid signatureThrown as PlatformApiError, never retried
401 (replay)A nonce that's already been usedJSON { "error": "replayed request" }Thrown as PlatformApiError, never retried
401 (identity)No provider identity resolved, or the session token is invalid/expiredJSON { "error": "..." }Thrown as PlatformApiError, never retried
403The session belongs to a different provider than the one that signed this requestJSON { "error": "..." }Thrown as PlatformApiError, never retried
404No matching route — check baseUrl and the pathJSON { "error": "..." }Thrown as PlatformApiError, never retried
409Another attempt for this transactionId is already in flightJSON { "error": "..." }Retried with backoff, same transactionId
429Rate limit exceeded (per-tenant)JSON { "error": "rate limit exceeded" }, Retry-After header (seconds)Thrown as PlatformApiError, not retried automatically — honor retryAfter yourself
500Retryable platform-side failure (includes a TIMED_OUT resolution)JSON { "error": "..." }Retried with backoff
503Game or operator adapter not configuredJSON { "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:

ts
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.

ts
class RequestTimeoutError extends Error {
  timeoutMs: number
  hint: string
}

isRetryableStatus

ts
function isRetryableStatus(status: number): boolean
// true for 409 and any status >= 500
// false for everything else, including 429

Exported 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.