Skip to main content

Error Handling

Saturday uses a consistent error format across all endpoints, modeled on Stripe’s error structure. Errors are predictable, machine-readable, and include enough context to diagnose the issue without consulting logs.

Error format

Every error response follows this structure:
{
  "error": {
    "type": "invalid_request",
    "code": "missing_required_field",
    "message": "The 'athlete_weight_kg' field is required for hydration calculations.",
    "param": "athlete_weight_kg",
    "documentation_url": "https://api.saturday.fit/docs/errors#missing_required_field",
    "request_id": "req_abc123def456"
  }
}
FieldAlways presentDescription
typeYesCategory of error
codeYesSpecific error code (machine-readable)
messageYesHuman-readable explanation
paramNoThe request parameter that caused the error
documentation_urlYesDirect link to docs for this error
request_idYesUnique identifier for this request — include in support tickets

Error types

TypeHTTP StatusDescription
authentication_error401Invalid, expired, or missing API key
authorization_error403Valid key but insufficient permissions
invalid_request400Request is malformed or missing required fields
resource_not_found404The requested resource doesn’t exist
conflict409Request conflicts with current state
rate_limit_exceeded429Too many requests
safety_limit422Calculation would violate safety guardrails
api_error500Something went wrong on Saturday’s end

Retry logic

Error typeRetry?Strategy
authentication_errorNoFix credentials
authorization_errorNoFix permissions
invalid_requestNoFix the request
resource_not_foundNoFix the resource ID
rate_limit_exceededYesWait for Retry-After header
safety_limitNoReview and adjust parameters
api_errorYesExponential backoff

Exponential backoff implementation

import time
import random

def request_with_retry(make_request, max_retries=3):
    for attempt in range(max_retries + 1):
        response = make_request()

        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue

        if response.status_code >= 500:
            if attempt < max_retries:
                delay = (2 ** attempt) + random.uniform(0, 1)
                time.sleep(delay)
                continue

        return response

    return response

Request IDs

Every response includes a request_id in both the response body and the X-Request-Id header. Always include this when reporting issues — it lets us trace the exact request through our systems.