Skip to content

Error Handling ​

All FAST API responses follow a consistent format. Every response includes a correlationId for tracing. This guide covers error formats, retry strategies, and resilience patterns.

Error response format ​

json
{
  "error": "error_code",
  "message": "Human-readable description of the problem",
  "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
FieldDescription
errorMachine-readable error code (use this for conditional logic)
messageHuman-readable description (use this for debugging, not for users)
correlationIdUnique request identifier for tracing across systems

Always log the correlationId. Include it when contacting FAST support.

HTTP status codes ​

StatusError codeDescriptionRetry?
400validation_errorMissing or invalid request fieldsNo. Fix the request.
401unauthorizedInvalid or missing API key, or expired JWTNo. Fix credentials.
402*_declinedYour approval URL declined the actionNo.
403approval_requiredWidget money action with no approval URL configuredNo. Configure one.
404not_foundResource does not existNo. Check the ID.
409VariousBusiness rule conflictNo. Handle the specific case.
422mapping_errorAdapter mapping failureNo. Fix mapping config.
429rate_limited, purchase_cooldownToo many requests, or a purchase before the cooldown endsYes. Respect Retry-After / cooldownUntil.
500internal_errorUnexpected server errorYes. Exponential backoff.
503maintenance_mode, service_unavailableFAST is in maintenance, or the idempotency store is down on a money actionYes. Short delay, then retry (same idempotency key).

Handling each error type ​

400 - Validation error ​

The request body failed validation. Check the message field for specifics.

Common causes:

  • Missing userId or challengeTierId
  • Invalid UUID format
  • Malformed JSON
  • Negative stakeAmount
  • Missing required fields for bet event
  • Missing X-Idempotency-Key on a withdrawal or stake refund (missing_idempotency_key)
  • Withdrawal above what can be taken now (insufficient_withdrawable_balance) or against a tier payout rule (below_min_payout, payout_interval, max_payouts_reached, first_payout_deadline, no_profit); reasons lists every one that applies
json
{
  "error": "validation_error",
  "message": "stakeAmount must be a positive number",
  "correlationId": "uuid"
}

Action: Fix the request payload. Do not retry with the same body.

401 - Unauthorized ​

json
{
  "error": "unauthorized",
  "message": "Invalid API key",
  "correlationId": "uuid"
}

Action: Verify your API key is correct and included in the X-API-Key header. For admin endpoints, check that your JWT has not expired (1-hour lifetime). Refresh the token if expired.

404 - Not found ​

json
{
  "error": "not_found",
  "message": "No active challenge or funded account for this user",
  "correlationId": "uuid"
}

Action: The resource does not exist. Common when validating a bet or withdrawing for a user with no active challenge or funded account. Handle gracefully in your UI (for bet validation, skip FAST for that user).

409 - Conflict ​

Business rule violations. The specific error code tells you what happened.

Error codeMeaningRecommended handling
active_challenge_existsUser already has an active challengeShow "challenge already active" in UI
challenge_awaiting_choiceUser passed and has not chosen continue or refundPrompt the user to choose
active_funded_account_existsUser has an active funded accountShow the funded account instead of the shop
tier_disabledChallenge tier is disabledRemove tier from purchase options
tier_not_foundNo such tier for your tenantRefresh the tier list

The full list for purchases is in Challenge Purchase.

Action: Do not retry. Read the error code and handle the specific case in your application logic.

422 - Mapping error ​

json
{
  "error": "mapping_error",
  "message": "Field 'bet_amount' could not be mapped to 'stakeAmount'",
  "correlationId": "uuid"
}

Action: Your adapter mapping is misconfigured. Fix it in the operator backoffice (Settings > Adapter Mapping). Test with the mapping preview tool before saving.

429 - Rate limited ​

json
{
  "error": "rate_limited",
  "message": "Too many requests. Limit: 500/min for this endpoint.",
  "retryAfter": 42,
  "correlationId": "uuid"
}

The response includes both a Retry-After header and a retryAfter field (seconds until the window resets).

Action: Back off and retry after the indicated time. See retry strategy below.

500 - Internal error ​

json
{
  "error": "internal_error",
  "message": "An unexpected error occurred",
  "correlationId": "uuid"
}

Action: Retry with exponential backoff (up to 3 attempts). If persistent, contact FAST support with the correlationId.

503 - Maintenance mode ​

json
{
  "error": "maintenance_mode",
  "message": "FAST is in maintenance mode. Read operations are available.",
  "correlationId": "uuid"
}

During maintenance:

  • Read operations (GET) continue to work
  • Write operations (POST, PUT, DELETE) are blocked
  • Maintenance windows are typically under 15 minutes

Action: Queue write requests and retry after a short delay. Do not block your sportsbook operations.

Retry strategy with exponential backoff ​

Use this pattern for 429, 500, and 503 responses:

typescript
async function callFastApi(request: () => Promise<Response>): Promise<Response> {
  const maxRetries = 3;
  const baseDelay = 1000; // 1 second

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await request();

    if (response.ok) return response;

    const status = response.status;

    // Do not retry client errors (except 429)
    if (status >= 400 && status < 500 && status !== 429) {
      return response;
    }

    // Retryable errors: 429, 500, 503
    if (attempt === maxRetries) return response;

    let delay: number;

    if (status === 429) {
      // Respect Retry-After header
      const retryAfter = response.headers.get('Retry-After');
      delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelay * Math.pow(2, attempt);
    } else {
      // Exponential backoff with jitter
      delay = baseDelay * Math.pow(2, attempt) + Math.random() * 500;
    }

    await new Promise(resolve => setTimeout(resolve, delay));
  }

  throw new Error('Unreachable');
}

Retry timing ​

AttemptBase delayWith jitter
11s1.0 - 1.5s
22s2.0 - 2.5s
34s4.0 - 4.5s

After 3 failed attempts, log the error with correlationId and alert your on-call team.

Circuit breaker pattern ​

If FAST is experiencing sustained issues, implement a circuit breaker to prevent cascading failures:

typescript
class FastCircuitBreaker {
  private failures = 0;
  private lastFailure = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';

  private readonly threshold = 5;       // Open after 5 failures
  private readonly resetTimeout = 30000; // Try again after 30s

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is open - FAST API unavailable');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }

  private onFailure() {
    this.failures++;
    this.lastFailure = Date.now();
    if (this.failures >= this.threshold) {
      this.state = 'open';
    }
  }
}

When the circuit is open, your sportsbook should:

  • Continue accepting bets normally (FAST is never a single point of failure)
  • Queue bet events for later delivery
  • Show challenges as "temporarily unavailable" in the UI
  • Log that the circuit opened for investigation

Graceful degradation ​

FAST should never block your core sportsbook operations. Design your integration to degrade gracefully:

FAST stateYour sportsbook behavior
HealthyNormal operation. Validate bets, track challenges.
Slow (> 2s latency)Skip validation, accept bets, queue for async processing.
Down (5xx)Accept all bets normally. Queue events. Reconcile later.
Maintenance (503)Read operations work. Queue writes. Show "updates paused" in UI.

Implementation pattern ​

typescript
async function validateBet(bet: BetRequest): Promise<BetDecision> {
  try {
    const response = await fastCircuitBreaker.call(() =>
      callFastApi(() => fetch(`${FAST_API}/v1/bets/validate`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': process.env.FAST_API_KEY,
        },
        body: JSON.stringify(bet),
        signal: AbortSignal.timeout(2000), // 2s timeout
      }))
    );

    const data = await response.json();
    return { status: data.validationResult.status, reasons: data.validationResult.reasonCodes };
  } catch (error) {
    // FAST unavailable - degrade gracefully
    logger.warn('FAST validation unavailable, accepting bet', {
      error: error.message,
      betId: bet.externalBetId,
    });

    // Queue for async processing
    await betQueue.add({ ...bet, status: 'pending-validation' });

    return { status: 'accepted', reasons: [] };
  }
}

Timeout handling ​

Set explicit timeouts on all FAST API calls:

EndpointRecommended timeoutRationale
/v1/bets/validate2sSynchronous, user-facing, must be fast
/v1/bets5sCan be async, less time-sensitive
/v1/challenges/purchase5sUser-initiated, can show spinner
/v1/users/:id/progress3sUI display, can show cached data
/v1/users/:id/withdraw10sFinancial operation, worth waiting

If a timeout fires:

  • For /bets/validate: accept the bet and queue for async validation
  • For /bets: queue and retry later (event will be processed eventually)
  • For /challenges/purchase: show an error and let the user retry
  • For /users/:id/withdraw: show "processing" and poll for status

Idempotency ​

Some FAST endpoints are naturally idempotent:

EndpointIdempotent?Key
POST /v1/betsYesexternalBetId + status
POST /v1/bets/validateYesStateless, no side effects
POST /v1/challenges/purchaseNoReturns 409 active_challenge_exists on duplicate
POST /v1/users/:id/withdrawYes, with the required headerX-Idempotency-Key
POST /v1/users/:id/challenge/refund-stakeYes, with the required headerX-Idempotency-Key
POST /v1/users/:id/funded/refund-stakeYes, with the required headerX-Idempotency-Key

For /v1/bets, resending an event with the same externalBetId and status is safe: FAST answers success and changes nothing.

The withdrawal and both refund-stake endpoints require an X-Idempotency-Key header (400 missing_idempotency_key without it). A repeat with the same key and body returns the stored response instead of paying again; the same key with a different body returns 409 idempotency_conflict, and a repeat while the first is still running returns 409 idempotency_in_progress (retry shortly). Keys are kept for 24 hours. If FAST cannot reach its idempotency store it answers 503 service_unavailable rather than risk a double payout; retry with the same key. The SDK sets the header for you. Any other POST also honours an X-Idempotency-Key if you send one.

Webhook error handling ​

Your webhook endpoint should:

  1. Return a 2xx within 5 seconds (FAST considers anything else a failure; see timeouts)
  2. Verify the HMAC-SHA256 signature over the raw body before processing
  3. Process the event asynchronously if it takes more than a few seconds
  4. Be idempotent (the same event may be delivered more than once during retries)
typescript
// express.raw keeps req.body as the exact bytes FAST signed
app.post('/webhooks/fast', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  if (!signature || !verifySignature(req.body, signature, WEBHOOK_SECRET)) {
    logger.error('Invalid webhook signature', { signature });
    return res.status(401).send('invalid signature');
  }

  // Acknowledge, then process asynchronously (dedupe on eventId)
  res.status(200).send('ok');
  await webhookQueue.add(JSON.parse(req.body.toString('utf8')));
});

verifySignature is the length-checked, constant-time comparison shown in Webhook Handling. Failed deliveries are retried; the schedule is in Webhook Handling.

Correlation ID usage ​

Every FAST response includes a correlationId. Use it for:

  1. Debugging: Link your logs to FAST's logs for a specific request
  2. Support tickets: FAST support can trace any request with this ID
  3. Reconciliation: Match webhook deliveries to originating API calls
typescript
// Log correlation IDs on every FAST API response
const response = await fetch(fastUrl, options);
const correlationId = response.headers.get('x-correlation-id');
const body = await response.json();

logger.info('FAST API response', {
  endpoint: fastUrl,
  status: response.status,
  correlationId: correlationId || body.correlationId,
  durationMs: Date.now() - startTime,
});

Support ​

BLACKLINE CONSULTING d.o.o.