Appearance
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"
}| Field | Description |
|---|---|
error | Machine-readable error code (use this for conditional logic) |
message | Human-readable description (use this for debugging, not for users) |
correlationId | Unique request identifier for tracing across systems |
Always log the correlationId. Include it when contacting FAST support.
HTTP status codes
| Status | Error code | Description | Retry? |
|---|---|---|---|
| 400 | validation_error | Missing or invalid request fields | No. Fix the request. |
| 401 | unauthorized | Invalid or missing API key, or expired JWT | No. Fix credentials. |
| 402 | *_declined | Your approval URL declined the action | No. |
| 403 | approval_required | Widget money action with no approval URL configured | No. Configure one. |
| 404 | not_found | Resource does not exist | No. Check the ID. |
| 409 | Various | Business rule conflict | No. Handle the specific case. |
| 422 | mapping_error | Adapter mapping failure | No. Fix mapping config. |
| 429 | rate_limited, purchase_cooldown | Too many requests, or a purchase before the cooldown ends | Yes. Respect Retry-After / cooldownUntil. |
| 500 | internal_error | Unexpected server error | Yes. Exponential backoff. |
| 503 | maintenance_mode, service_unavailable | FAST is in maintenance, or the idempotency store is down on a money action | Yes. 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
userIdorchallengeTierId - Invalid UUID format
- Malformed JSON
- Negative
stakeAmount - Missing required fields for bet event
- Missing
X-Idempotency-Keyon 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);reasonslists 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 code | Meaning | Recommended handling |
|---|---|---|
active_challenge_exists | User already has an active challenge | Show "challenge already active" in UI |
challenge_awaiting_choice | User passed and has not chosen continue or refund | Prompt the user to choose |
active_funded_account_exists | User has an active funded account | Show the funded account instead of the shop |
tier_disabled | Challenge tier is disabled | Remove tier from purchase options |
tier_not_found | No such tier for your tenant | Refresh 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
| Attempt | Base delay | With jitter |
|---|---|---|
| 1 | 1s | 1.0 - 1.5s |
| 2 | 2s | 2.0 - 2.5s |
| 3 | 4s | 4.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 state | Your sportsbook behavior |
|---|---|
| Healthy | Normal 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:
| Endpoint | Recommended timeout | Rationale |
|---|---|---|
/v1/bets/validate | 2s | Synchronous, user-facing, must be fast |
/v1/bets | 5s | Can be async, less time-sensitive |
/v1/challenges/purchase | 5s | User-initiated, can show spinner |
/v1/users/:id/progress | 3s | UI display, can show cached data |
/v1/users/:id/withdraw | 10s | Financial 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:
| Endpoint | Idempotent? | Key |
|---|---|---|
POST /v1/bets | Yes | externalBetId + status |
POST /v1/bets/validate | Yes | Stateless, no side effects |
POST /v1/challenges/purchase | No | Returns 409 active_challenge_exists on duplicate |
POST /v1/users/:id/withdraw | Yes, with the required header | X-Idempotency-Key |
POST /v1/users/:id/challenge/refund-stake | Yes, with the required header | X-Idempotency-Key |
POST /v1/users/:id/funded/refund-stake | Yes, with the required header | X-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:
- Return a 2xx within 5 seconds (FAST considers anything else a failure; see timeouts)
- Verify the HMAC-SHA256 signature over the raw body before processing
- Process the event asynchronously if it takes more than a few seconds
- 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:
- Debugging: Link your logs to FAST's logs for a specific request
- Support tickets: FAST support can trace any request with this ID
- 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
- API documentation: https://docs.fundedfast.eu/api/endpoints
- Email: support@fundedfast.eu
- Phone: +385 99 371 341
- Always include the
correlationIdwhen reporting issues