Developers

Error handling

The SchneeAI Gateway returns OpenAI-compatible error objects with SchneeAI-specific extensions (budget, PII, routing). Here is how to parse them, when to retry, and how to make retries safe.

Design-partner phase. The public api.schneeai.com endpoint is activated at general availability. The error envelopes and status codes below are the production contract; until GA, exercise them against your partner-specific endpoint.

Error response shape

Every non-2xx response has the same envelope:

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 2 seconds.",
    "param": null,
    "request_id": "req_01HQ..."
  }
}
FieldPurpose
typeOpenAI-compatible category (invalid_request_error, authentication_error, rate_limit_error, api_error)
codeMachine-readable sub-code (see SchneeAI-specific codes)
messageHuman-readable explanation
paramOffending parameter, if applicable
request_idInclude this when contacting support — we can trace the exact call

HTTP status → action

StatusMeaningRetry?Action
400Malformed JSON or missing required fieldNoFix the request body
401API key invalid or missingNoCheck the Authorization header
403Tenant scope or role forbiddenNoVerify tenant and role on the token
404model or prompt name not foundNoCheck the Model Directory
422Valid JSON but semantically rejectedNoRead error.code — usually context length, unsupported parameter, or content policy
402Budget or credits exhaustedNoTop up credits or wait for the budget reset window
429Rate limitYes (with backoff)Honor Retry-After if present
500Internal Gateway errorYesExponential backoff
502Upstream provider errorYesExponential backoff
503Gateway temporarily unavailableYesExponential backoff
504Upstream provider timeoutYesExponential backoff

SchneeAI-specific error codes

The OpenAI-compatible envelope carries SchneeAI-specific sub-codes in error.code. The ones you will actually hit:

CodeHTTPMeaning
insufficient_credits402The tenant or feature budget is exhausted, or the credit account is depleted. Internal SchneeAI codes are BUDGET_EXCEEDED and CREDIT_INSUFFICIENT; on the wire both surface as insufficient_credits. Wait for the reset window (next hour / day / month, per policy) or top up credits.
pii_detected422Your tenant’s PII policy is set to block and the scanner found a critical category. Redact the input or switch the policy to mask / flag.
context_length_exceeded422The combined prompt + max_tokens exceeds the model’s context window. Truncate or switch to a longer-context model.
unsupported_parameter422The selected provider does not support a parameter you sent (e.g., temperature with reasoning models).
provider_failed502The upstream LLM provider returned an error. Retry with backoff.
provider_timeout504The upstream LLM provider timed out after the retry budget was exhausted. Partial Vault write may have occurred for streamed output.
jwks_unavailable503The Gateway could not reach the JWT keyset (Ory Hydra). The Gateway does not fall back to a cached key beyond its TTL. Retry with backoff.

Retry strategy

Retry only on 429, 500, 502, 503, 504. Use exponential backoff with jitter:

wait = min(base * 2^attempt, max_wait) + random_jitter
  base      = 1 second
  max_wait  = 60 seconds
  jitter    = 0–25% of wait
  attempts  = max 5

Honor the Retry-After header when present. 429 responses include it as an integer number of seconds.

When not to retry

  • 400 / 401 / 403 / 404 / 422 — the request itself is wrong; retrying produces the same error.
  • 402 with code: insufficient_credits — budget exhausted or credits depleted. Retrying burns budget/credits faster. Wait for the reset window or top up.

Idempotency

For POST /v1/chat/completions, send an Idempotency-Key header so retries do not double-charge or double-count:

curl https://api.schneeai.com/v1/chat/completions \
  -H "Authorization: Bearer $SCHNEEAI_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "model": "gpt-4o-mini", "messages": [...] }'
  • Format: any string ≤ 255 chars. UUIDv4 recommended.
  • TTL: 24 hours. After that the same key is treated as a new request.
  • Scope: scoped to your service + tenant, so two tenants using the same key do not collide.
  • What is deduplicated: the charge and the usage record. The actual LLM call still happens if the original request never reached the provider.

Client examples

Python (requests + tenacity)

import uuid
import requests
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type

class RetryableError(Exception): pass

@retry(stop=stop_after_attempt(5),
       wait=wait_exponential_jitter(initial=1, max=60, jitter=2),
       retry=retry_if_exception_type(RetryableError))
def chat(messages, model="gpt-4o-mini"):
    res = requests.post(
        "https://api.schneeai.com/v1/chat/completions",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Idempotency-Key": str(uuid.uuid4()),
            "Content-Type": "application/json",
        },
        json={"model": model, "messages": messages},
        timeout=60,
    )
    if res.status_code in {429, 500, 502, 503, 504}:
        raise RetryableError(res.text)
    res.raise_for_status()
    return res.json()

TypeScript (fetch)

async function chat(messages: Msg[], model = "gpt-4o-mini") {
  const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch("https://api.schneeai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Idempotency-Key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ model, messages }),
    });
    if (res.ok) return res.json();
    if (![429, 500, 502, 503, 504].includes(res.status)) {
      const body = await res.json();
      throw new Error(`schneeai ${res.status}: ${body.error?.code}`);
    }
    const retryAfter = Number(res.headers.get("Retry-After")) || 0;
    const wait = retryAfter || Math.min(2 ** attempt + Math.random(), 60);
    await sleep(wait * 1000);
  }
  throw new Error("schneeai: max retries exceeded");
}

Go (net/http + backoff)

var buf bytes.Buffer
json.NewEncoder(&buf).Encode(map[string]any{
    "model":    "gpt-4o-mini",
    "messages": messages,
})
req, _ := http.NewRequest("POST", "https://api.schneeai.com/v1/chat/completions", &buf)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Idempotency-Key", uuid.NewString())
req.Header.Set("Content-Type", "application/json")

bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 60 * time.Second
backoff.Retry(func() error {
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    defer resp.Body.Close()
    switch {
    case resp.StatusCode >= 500, resp.StatusCode == 429:
        return errors.New("retry: " + resp.Status)
    case resp.StatusCode >= 400:
        return backoff.Permanent(parseError(resp))
    }
    return json.NewDecoder(resp.Body).Decode(&result)
}, bo)

Support

If an error persists after correct retrying, email [email protected] with the request_id from the error response. We can trace the exact call within minutes.


Note: SchneeAI is in design-partner stage. The error envelope above is stable and OpenAI-compatible; the budget / PII sub-codes are aligned with the platform features currently shipping. If you hit a code not listed here, send us the request_id and we will document it.