Error catalog
Every error Ringside returns, the envelope it arrives in and the eight type families that group it. The complete code list is grouped by area (auth, customers, chat, rate limits, wallet, vector stores, webhooks, assistants and runs, batch, files) with the HTTP status, the type and exactly how to handle each one. Branch on code, not on the human message, and log the X-Request-Id response header on every failure.
The mental model
Every non-2xx response from https://api.fightclub.pro/v1 carries a single JSON object with one top-level error key. The shape is stable across all endpoints. You read three fields on every error and two more on specific families. The type tells you the broad class and usually maps 1:1 to the HTTP status. The code is the stable machine-readable string you branch on. The message is human text for your logs and can change without notice.
The message field is free text tuned for a human reading a log line. It is not part of the API contract and we reword it whenever it reads better. The code is the contract. Write if (err.code === 'wallet_empty'), never a substring match on the message.
The error envelope
Three fields are always present: type, code and message. param appears only on field-validation errors and names the offending request field. retry_after_seconds appears only on rate-limit errors and mirrors the integer Retry-After response header.
{
"error": {
"type": "invalid_request_error",
"code": "invalid_model_ref",
"message": "model is not a fc:/match:/slot:/dyn: ref.",
"param": "model"
}
}- type
- Always present. One of the eight families below (plus
not_implementedfor endpoints still on the roadmap). Maps to the HTTP status in almost every case. - code
- Always present. The stable identifier you branch on. Unique per failure mode.
- message
- Always present. Human-readable detail for logs. Not a stable contract; do not parse it.
- param
- Present on field-validation errors only. The name of the request field that failed (
model,messages,metadata, etc.). - retry_after_seconds
- Present on rate-limit errors only. Integer seconds to wait before retrying. Mirrors the
Retry-Afterheader on the same response.
The request id
Every response, success or failure, carries an X-Request-Id response header (value prefix req_). It is a header, never a body field. Capture it on failures and quote it in support requests. If you set your own X-Request-Id on the request, Ringside echoes yours back so you can correlate against your own traces.
# Pull the request id off any response
curl -sD - https://api.fightclub.pro/v1/chat/completions \
-H "Authorization: Bearer ko_REDACTED" \
-H "Content-Type: application/json" \
-d '{"model":"fc:openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' \
| grep -i '^x-request-id:'
# x-request-id: req_8f2c1a...Type families
Eight families cover the catalogue, with not_implemented reserved for roadmap endpoints. The family is a coarse signal: it tells your retry logic whether the failure is yours to fix (4xx) or ours to recover from (5xx). For anything beyond a retry/no-retry decision, switch on the code.
| type | Usual HTTP | What it means | Retry? |
|---|---|---|---|
authentication_error | 401 | The Authorization header is missing, malformed or the key/token is invalid, revoked or expired. | No. Fix the credential. |
invalid_request_error | 400 (also 404, 413, 422) | A field, header or body is wrong: bad shape, missing required field, over a limit, unsupported value. | No. Fix the request. |
permission_error | 403 | The credential is valid but lacks the scope, or a client token tried something only a developer key may do. | No. Grant scope or use a developer key. |
rate_limit_error | 429 | One of the four rate-limit layers fired. Read retry_after_seconds / Retry-After. | Yes, after the advertised wait. |
conflict_error | 409 | The resource is in a state that forbids the operation: name taken, thread locked, run already terminal, file in use. | Sometimes, once the conflicting state clears. |
not_found_error | 404 | No such resource. Also returned for cross-tenant access so ids are not enumerable. | No. |
payment_required | 402 | A wallet or budget is exhausted: developer wallet, customer prepaid wallet or customer monthly budget. | No. Top up or raise the budget. |
api_error | 500, 503 | Something on our side failed: a provider was unreachable, no provider key is configured or an internal error. | Yes, with backoff. |
not_implemented | 501 | The endpoint exists in the spec but is not live yet (e.g. POST /v1/realtime). | No. |
Reading a cus_, vs_, asst_, batch_ or any other id that belongs to a different developer returns not_found_error (404), never permission_error (403). The 404 is deliberate so an attacker cannot enumerate which ids exist.
The wallet write paths (/wallet/topup, /wallet/adjust) can return code: wallet_error with type: api_error and a 500 when the ledger write fails server-side. The day-to-day "out of money" cases are wallet_empty, customer_wallet_empty and customer_budget_exceeded, all type: payment_required at 402.
Complete code reference
Grouped by area. Status is the HTTP code, type is the envelope family. Codes shared across many endpoints (invalid_api_key, not_found, malformed_json, metadata_too_large) are listed once under the area where you hit them first, but they behave identically everywhere.
Authentication and authorization
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
missing_token | 401 | authentication_error | No Authorization header on the request. | Send Authorization: Bearer ko_... (developer key) or Authorization: Client <jwt> (browser client token). |
invalid_auth_scheme | 401 | authentication_error | Authorization header did not start with Bearer or Client . | Use one of the two schemes. Bearer for ko_ keys, Client for client-token JWTs. |
invalid_token_format | 401 | authentication_error | A Bearer token that does not start with the ko_ prefix. | Use a real developer key from /app/api-keys. Format is ko_ plus 64 hex chars. |
invalid_token | 401 | authentication_error | The token is unrecognised, revoked or expired. | Rotate the key in the dashboard, or re-mint the client token. |
invalid_api_key | 401 | authentication_error | Per-endpoint surface of a missing or revoked key/token (the form the endpoint reference advertises). | Same as invalid_token: fix the credential. |
invalid_client_token | 401 | authentication_error | The Client-scheme JWT is empty, failed signature verification, was revoked or is unknown. | Mint a fresh token via POST /v1/customers/{id}/client_tokens (the ttl is 60..3600s, default 900). |
origin_not_allowed | 403 | permission_error | A client token with an origin_allowlist was used from an Origin not on the list. | Add the Origin when minting, or call from an allowed Origin. |
ip_binding_violation | 403 | permission_error | A bind_ip client token was used from a different client IP. | Mint per client IP, or drop bind_ip for roaming clients. |
endpoint_not_allowed_for_client_token | 403 | permission_error | A client token hit a developer-only endpoint (e.g. /v1/files, or revoking another client token). | Call developer-only endpoints from your server with the ko_ key, not the browser. |
insufficient_scope | 403 | permission_error | The credential is valid but lacks the required scope (e.g. minting client tokens needs api:client_tokens). | Grant the scope to the key, or use a key that holds it. |
client_token_rate_limit_exceeded | 429 | rate_limit_error | The per-client-token rpm window is exhausted. Carries retry_after_seconds. | Back off until the advertised retry, then resume. Raise the token rpm at mint time (max 600, clamped to the customer rpm). |
Customers
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
invalid_rpm_limit | 400 | invalid_request_error | rpm_limit is not a positive integer (also: client-token rpm_limit outside 1..600). | Send a positive integer. |
invalid_tpm_limit | 400 | invalid_request_error | tpm_limit is not a positive integer. | Send a positive integer. |
invalid_monthly_budget_usd | 400 | invalid_request_error | monthly_budget_usd is negative or not a number. | Send a non-negative number, or omit for no budget. |
invalid_default_billable_markup_pct | 400 | invalid_request_error | default_billable_markup_pct is negative or not a number. | Send a non-negative number. |
invalid_default_billable_amount_usd | 400 | invalid_request_error | default_billable_amount_usd is negative or not a number. | Send a non-negative number. |
billable_defaults_mutex | 400 | invalid_request_error | You set both default_billable_markup_pct and default_billable_amount_usd. They are mutually exclusive. | Set at most one of the two. |
empty_patch | 400 | invalid_request_error | A defaults PATCH carried none of the updatable fields. | Include at least one field to change. |
invalid_lazy_create_customers | 400 | invalid_request_error | lazy_create_customers is not a boolean. | Send true or false. |
metadata_too_large | 400 | invalid_request_error | metadata over 4 KB serialized (customers and vector stores; conversations cap at 2 KB). | Trim metadata. Limits: 16 keys, key <=64 chars, value <=512 chars. |
invalid_metadata | 400 | invalid_request_error | metadata is not a JSON object, or breaks the key/length limits. | Send a flat object inside the limits above. |
external_id_taken | 409 | conflict_error | You already have a customer with that external_id. | Reuse the existing customer, or pick a different external_id. |
customer_not_found | 404 | not_found_error | The FC-Customer id/external id is unknown and lazy-create is off (or FC-Customer-Strict: true was set). | Create the customer first, or drop strict mode to lazy-create. |
not_found | 404 | not_found_error | No customer with that id or external id. Also returned for cross-tenant access. | Check the id. A 404 here does not confirm the id exists for someone else. |
invalid_status | 400 | invalid_request_error | A conversation list filter status was not active, locked or archived. | Use one of the three values. |
invalid_status_transition | 400 | invalid_request_error | A conversation PATCH tried to set status to something other than active or archived (you cannot write locked). | Only move between active and archived. locked is managed by the lock primitive. |
metadata_too_large (conversation) | 400 | invalid_request_error | Conversation metadata exceeds the 2 KB serialized cap. | Trim conversation metadata to under 2 KB. |
conversation_full | 409 | conflict_error | The conversation/thread hit the 5,000-message cap. | Start a new conversation. |
conversation_locked | 409 | conflict_error | A message request holds the conversation lock (TTL 10 min). | Retry in ~10 s once the in-flight turn finishes. |
Chat completions
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
invalid_json | 400 | invalid_request_error | The request body is not valid JSON (also surfaced as malformed_json on some endpoints). | Send valid JSON with Content-Type: application/json. |
missing_model | 400 | invalid_request_error | model is absent or not a string/array. | Send a model ref or an array of refs. |
missing_messages | 400 | invalid_request_error | messages is absent or empty. | Send a non-empty messages array. |
invalid_model_ref | 400 | invalid_request_error | model is a bare name, or carries an unknown @region suffix. Every ref needs a fc: / match: / slot: / dyn: prefix. | Prefix it, e.g. fc:openai/gpt-4o-mini. Region suffixes attach to fc: refs only. |
invalid_model_for_chat | 400 | invalid_request_error | The ref resolved to a model that cannot do chat completions. | Pick a chat-capable model. |
unknown_slot | 400 | invalid_request_error | A slot:<alias> ref names an alias you have not defined. | Define the slot, or use a fc:/match: ref. |
slot_cycle | 400 | invalid_request_error | A slot: alias resolves through a cycle. | Break the alias loop in your slot config. |
moderation_flagged | 400 | invalid_request_error | The prompt or completion failed inline moderation (the chat moderation param). | Drop or rewrite the flagged content. Pre-screen with POST /v1/moderations (free). |
streaming_with_schema_fallback_unsupported | 400 | invalid_request_error | response_format json_schema requested together with stream: true. | Stream without a schema, or use a schema without streaming. |
fallback_with_stream_unsupported | 400 | invalid_request_error | A model: [...] cost cascade combined with stream: true. | Stream with a single model, or run the cascade non-streamed. |
fallback_with_idempotency_unsupported | 400 | invalid_request_error | A model: [...] cascade combined with an idempotency-key. | Use a single model for idempotent calls. |
response_schema_validation_failed | 422 | invalid_request_error | Output did not satisfy response_format.json_schema after the re-prompt retries. | Loosen the schema, raise max_tokens or switch model. |
platform_not_configured | 503 | api_error | No upstream provider key is configured for the resolved model. | Pick a model whose provider is configured. Retry is pointless until config changes. |
upstream_unavailable | 503 | api_error | The provider errored and no fallback in the cascade succeeded. | Retry with backoff, or add a model: [...] fallback chain. |
internal_error | 500 | api_error | An unhandled server-side failure. | Retry with backoff. Quote the X-Request-Id if it persists. |
The embeddings and rerank endpoints reuse the same auth and wallet codes plus their own validation: missing_input, invalid_input and input_too_large on embeddings (array <=2,048 items, each <=100,000 chars), and unsupported_rerank_model and too_many_documents on rerank (<=1,000 documents). Moderation only returns invalid_api_key, invalid_json and missing_input, and meters at $0.
Rate limits
Four enforcement layers run, lowest ceiling wins: edge per-IP, per-client-token, per-developer and per-customer. A 429 always carries an integer Retry-After header, and the body carries retry_after_seconds. Successful responses carry X-RateLimit-Remaining. There is no X-RateLimit-Limit or X-RateLimit-Reset.
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
edge_rate_limit_exceeded | 429 | rate_limit_error | The Layer 0 edge limiter (keyed by IP plus token, so it covers the per-IP and per-developer ceilings) fired before the request reached a route handler. | Sleep for Retry-After seconds, then retry. Spread load. |
customer_rate_limit_exceeded | 429 | rate_limit_error | The attributed customer hit its rpm_limit or tpm_limit. | Back off per retry_after_seconds. Raise the customer caps if the workload is legitimate. |
client_token_rate_limit_exceeded | 429 | rate_limit_error | The per-client-token rpm window is exhausted. | Back off. Mint with a higher rpm_limit (max 600, clamped to the customer rpm). |
mint_rate_limit | 429 | rate_limit_error | More than 100 client-token mints/min for one customer. | Throttle minting. Cache and reuse tokens up to their ttl. |
lazy_create_rate_exceeded | 429 | rate_limit_error | Too many customers auto-created via the FC-Customer header in a short window. | Bulk-create with POST /v1/customers instead of relying on lazy-create. |
Wallet and budget (402)
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
wallet_empty | 402 | payment_required | Your developer wallet balance is zero or insufficient for the call. | Top up the developer wallet. |
customer_wallet_empty | 402 | payment_required | The attributed customer's prepaid wallet is zero or insufficient. | Top up via POST /v1/customers/{id}/wallet/topup. |
customer_budget_exceeded | 402 | payment_required | The attributed customer spent its monthly_budget_usd for the period. | Raise the budget, or wait for the next month. |
vector_store_suspended | 402 | payment_required | A vector store is suspended for non-payment. | Settle the balance to reactivate the store. |
invalid_amount | 400 | invalid_request_error | A wallet adjust amount was zero or not a finite number. | Send a non-zero finite number. |
amount_too_large | 400 | invalid_request_error | A wallet adjust magnitude exceeded the per-call cap. | Split the adjustment into smaller amounts. |
wallet_error | 500 | api_error | A wallet ledger write failed server-side (topup/adjust). | Retry with backoff. The id-empotent retry is safe if you send the same idempotency-key. |
Every metered response (success or some 402s) carries X-Ringside-Wallet-Balance and X-Ringside-Wallet-Deducted so you can watch the balance fall and pre-empt the empty-wallet 402 before it bites.
Vector stores and RAG
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
invalid_name | 400 | invalid_request_error | name is empty. | Send a non-empty name. |
name_too_long | 400 | invalid_request_error | name exceeds 256 bytes. | Shorten the name. |
missing_embedding_model | 400 | invalid_request_error | No embedding_model on create, or on a migrate call. | Set embedding_model (fixed at create; change via POST .../migrate). |
invalid_embedding_model | 400 | invalid_request_error | embedding_model is not in the supported set. | Pick a supported embedding model. |
invalid_dimensions | 400 | invalid_request_error | dimensions is not a positive integer. | Send a positive integer, or omit for the model default. |
invalid_encryption | 400 | invalid_request_error | encryption is not none, managed or byok. | Use one of the three values. |
invalid_vector_sealing | 400 | invalid_request_error | vector_sealing is not none, source or full. | Use one of the three. Defaults to full when sealed. |
invalid_vector_index | 400 | invalid_request_error | vector_index is not auto, flat or ivf. | Use one of the three. |
ivf_requires_encryption | 400 | invalid_request_error | vector_index: ivf requested on an unsealed store. | Use ivf only on a sealed (encrypted) store. |
byok_key_required | 400 | invalid_request_error | encryption: byok without a key object. | Provide key: {type: passphrase, passphrase}. |
byok_passphrase_too_short | 400 | invalid_request_error | The byok passphrase is under 12 characters. | Use a passphrase of 12+ chars. |
byok_kms_unavailable | 400 | invalid_request_error | The byok KMS backend is not configured or reachable. | Retry once KMS is available, or switch to managed encryption. |
not_a_byok_store | 400 | invalid_request_error | An unlock/key call against a store that does not use byok. | Only present a key on byok stores. |
invalid_passphrase / missing_passphrase | 400 | invalid_request_error | Incorrect or absent passphrase when taking a key lease. | Provide the correct key: {passphrase}. |
vector_store_locked | 409 | conflict_error | A byok store has no active key lease. | Take a lease via POST .../unlock or send the FC-Vector-Store-Key header. |
vector_store_unavailable | 409 | conflict_error | The store is in a deleting or deleted state. | Do not write to a store being deleted. |
vector_store_not_found | 404 | not_found_error | No such store. Also returned for cross-tenant access. | Check the vs_ id. |
vector_store_file_not_found / file_batch_not_found | 404 | not_found_error | The file row or file batch is not attached to this store. | Check the id belongs to the store. |
file_not_found | 404 | not_found_error | A referenced file_id does not belong to you. | Upload the file first, or fix the id. |
file_not_cancellable / file_not_retryable | 409 | conflict_error | A cancel/retry hit an ingest file in the wrong state. | Cancel only pending/in_progress; retry only failed/cancelled. |
invalid_file_ids / too_many_files | 400 | invalid_request_error | A file batch had an empty file_ids, or more than 500 files. | Send a non-empty array of <=500 files. |
migration_not_found | 404 | not_found_error | Unknown migration id on a rollback. | Check the migration id. |
Webhooks
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
invalid_url | 400 | invalid_request_error | url is missing or empty. | Send a non-empty https URL. |
insecure_webhook_url | 400 | invalid_request_error | url is not https. | Use an https endpoint. |
malformed_webhook_url | 400 | invalid_request_error | url is unparseable or resolves to a private / SSRF-unsafe host. | Use a public, resolvable https URL. |
empty_events | 400 | invalid_request_error | events is missing or an empty array. | Subscribe to at least one event type. |
unknown_event_type | 400 | invalid_request_error | events contains a type Ringside does not emit (e.g. a reserved run.* type). | Use a type from the published taxonomy. |
invalid_rotation_grace | 400 | invalid_request_error | rotation_grace_minutes is out of range. | Send a value inside the allowed range. |
webhook_url_in_use | 409 | conflict_error | You already registered this url. URLs are unique per developer. | Reuse the existing endpoint, or use a different url. |
invalid_status_transition | 400 | invalid_request_error | A PATCH tried to move status somewhere other than active <-> paused (disabled_after_failures is worker-managed). | Only flip between active and paused. |
delivery_not_retryable | 409 | conflict_error | A manual retry against a delivery that is not in the dead state. | Only retry dead deliveries. |
not_found | 404 | not_found_error | No such endpoint or delivery. Also cross-tenant. | Check the wh_ id. |
These are errors from the webhook management API. They are separate from delivery signature verification, which happens on your receiver: validate the X-FC-Signature header (t=<unix>,v1=<hex>) constant-time, accept either digest during a rotation grace and reject stale timestamps. See Receive and verify webhooks.
Assistants, threads and runs
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
invalid_model | 400 | invalid_request_error | model is missing or not a valid ref (assistant create, or run override). | Send a valid fc:/match:/slot:/dyn: ref. |
invalid_tools / invalid_tool_schema / invalid_tool_shape | 400 | invalid_request_error | tools is not an array of valid tool objects, or a function tool's parameters schema is malformed. | Send {type: file_search} or {type: function, function} objects with valid JSON Schema. |
invalid_temperature / invalid_top_p | 400 | invalid_request_error | temperature outside 0-2, or top_p outside 0-1. | Clamp to the valid range. |
invalid_name | 400 | invalid_request_error | Assistant name is not a valid string. | Send a valid name. |
assistant_name_in_use | 409 | conflict_error | You already have an active assistant with this name. | Reuse it, or pick a unique name. |
invalid_messages | 400 | invalid_request_error | A seed thread message is malformed. | Fix the message shape. |
thread_full | 409 | conflict_error | Appending would exceed the 5,000-message thread cap. | Start a new thread. |
thread_locked | 409 | conflict_error | A run is already active on the thread (only one active run per thread). | Wait for the run to reach a terminal state before writing or starting another. |
invalid_assistant_id / invalid_additional_messages | 400 | invalid_request_error | A run's assistant_id or additional_messages is missing or malformed. | Fix the run body. |
invalid_tool_call_id | 400 | invalid_request_error | submit_tool_outputs referenced an unknown tool_call_id. | Answer only the tool_call_ids the run paused on. |
run_not_in_requires_action | 409 | conflict_error | submit_tool_outputs called while the run is not waiting for tool outputs. | Only submit when status is requires_action. |
run_already_completed | 409 | conflict_error | Cancel called on a run already in a terminal state. | No action; the run is done. |
not_found | 404 | not_found_error | No such assistant, thread or run. Also cross-tenant. | Check the asst_ / thread_ / run_ id. |
Batch
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
missing_input_file_id | 400 | invalid_request_error | input_file_id is absent. | Upload a JSONL with purpose=batch first, then pass its id. |
invalid_endpoint | 400 | invalid_request_error | endpoint is not /v1/chat/completions, /v1/embeddings or /v1/moderations. | Use one of the three supported targets. |
invalid_completion_window | 400 | invalid_request_error | completion_window is not 24h. | Send 24h (the only supported window). |
input_file_wrong_purpose | 400 | invalid_request_error | The input file was not uploaded with purpose=batch. | Re-upload the file with purpose=batch. |
metadata_too_large / metadata_too_many_keys | 400 | invalid_request_error | metadata over 4 KB, or more than 16 keys. | Trim metadata. Max 16 keys, key <=64 chars, value <=512 chars. |
batch_not_cancellable | 409 | conflict_error | Cancel against a batch that is not in a cancellable state. | Cancel only while the batch is running. |
not_found | 404 | not_found_error | No such batch. Also cross-tenant. | Check the batch_ id. |
A batch caps at 50,000 requests. Failed lines land in error_file_id; download both output_file_id and error_file_id via GET /v1/files/{id}/content.
Files
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
missing_purpose | 400 | invalid_request_error | purpose is required and absent. | Send purpose (e.g. batch, attachments, vision). |
invalid_purpose | 400 | invalid_request_error | purpose is not an allowed value. | Use a supported purpose. |
missing_file / missing_filename | 400 | invalid_request_error | No file part in the multipart body, or the part has no filename. | Send a multipart/form-data body with a named file part. |
invalid_multipart | 400 | invalid_request_error | The body is not valid multipart/form-data. | Send a proper multipart upload. |
file_too_large | 413 | invalid_request_error | Over the 512 MB per-file cap. | Split or shrink the file. |
file_in_use | 409 | conflict_error | DELETE blocked: the file is still referenced (by a vector store or batch). | Delete or cancel the referencing batch/store first. |
not_found | 404 | not_found_error | No such file. Also cross-tenant. | Check the file_ id. |
Inline media on chat is bounded too: images 20 MB (max 2 per message), PDFs/docs 20 MB (max 2 per message), audio 25 MB, and a serialized message body caps at 100,000 bytes. Over a limit you get an invalid_request_error (content_part_too_large, content_part_limit_exceeded, content_part_file_not_found or content_parts_unsupported_by_model).
Brains
| code | HTTP | type | Meaning | How to handle |
|---|---|---|---|---|
resource_not_found | 404 | not_found_error | The brain (or entity) does not exist, is not yours, or the feature is disabled. Cross-tenant collapses to this. | Check the brn_ id and that Brains is enabled for your account. |
missing_query | 400 | invalid_request_error | query is absent on recall. | Send a non-empty query. |
missing_scope | 400 | invalid_request_error | scope is required on GET /scopes and GET /entities and was absent. | Pass a scope query param; it bounds the listing to a subtree. |
missing_target | 400 | invalid_request_error | forget got none of scope/source/entity. | Provide exactly one target. |
missing_body | 400 | invalid_request_error | An entry write had no body. | Send a non-empty body. |
entry_too_large | 413 | invalid_request_error | An entry body exceeds the 100 MB limit. | Split the document into smaller entries. |
idempotency_key_reuse | 409 | invalid_request_error | The idempotency-key was reused with a different request body. | Use a fresh key for a different body, or resend the identical body to replay. |
entry_read_only | 409 | invalid_request_error | The entry comes from a reference mount; it cannot be edited or deleted in place. | Edit it in the source brain, or unmount the pack. |
preview_stale | 409 | invalid_request_error | The affected set changed between a forget dry_run and executing its preview_token. | Re-run the dry_run to get a fresh preview_token. |
invalid_preview_token | 400 | invalid_request_error | A forget preview_token is malformed or targets a different set. | Use the token returned by the matching dry_run. |
brain_unfunded | 402 | invalid_request_error | The account is unfunded and the brain has on_unfunded:reject (surfaced only on observe?wait=true). | Fund the account, or set on_unfunded to fallback/recall_only. |
brain_has_mounts | 409 | invalid_request_error | Reindex was called on a brain with live pack mounts (they share the embedding space). | Unmount all packs, then reindex. |
reindex_in_progress | 409 | invalid_request_error | A reindex is already running for this brain. | Poll the existing job; only one runs at a time. |
reindex_not_rollbackable | 409 | invalid_request_error | Rollback was called on a job that is not completed. | Only a completed reindex can be rolled back. |
rollback_window_expired | 409 | invalid_request_error | The 7-day reindex rollback window has passed. | The prior embedding space has been garbage-collected; reindex again if needed. |
Worked example: handling the common failures
A real client catches the credential, validation, rate-limit and wallet families separately, captures the request id and retries only the recoverable ones. This is the minimum production-grade handler.
import time
from openai import OpenAI, APIStatusError
client = OpenAI(
base_url="https://api.fightclub.pro/v1",
api_key="ko_REDACTED",
)
def chat_with_retry(messages, attempts=4):
for i in range(attempts):
try:
return client.chat.completions.create(
model="fc:openai/gpt-4o-mini",
messages=messages,
extra_headers={"FC-Customer": "cus_42"},
)
except APIStatusError as e:
req_id = e.response.headers.get("x-request-id")
err = (e.body or {}).get("error", {})
code = err.get("code")
if e.status_code in (401, 403):
raise RuntimeError(f"auth/permission: {code} (req {req_id})")
if e.status_code == 402:
# wallet_empty / customer_wallet_empty / customer_budget_exceeded
raise RuntimeError(f"out of funds: {code} (req {req_id})")
if e.status_code == 429:
wait = int(e.response.headers.get("retry-after", "1"))
time.sleep(wait)
continue
if 500 <= e.status_code < 600:
time.sleep(2 ** i) # backoff on api_error
continue
# 400/404/409/422 are your bug: do not retry
raise RuntimeError(f"bad request: {code} param={err.get('param')} (req {req_id})")
raise RuntimeError("exhausted retries")Retry policy by status
| Status | Retry? | Rule |
|---|---|---|
| 400 / 404 / 409 / 413 / 422 | No | Your request is wrong or the resource is in a forbidding state. Fix it; retrying repeats the same failure. |
| 401 / 403 | No | Fix the credential or scope first. |
| 402 | No | Top up the wallet or raise the budget. Watch X-Ringside-Wallet-Balance to pre-empt it. |
| 429 | Yes | Sleep for the integer Retry-After (or retry_after_seconds), then retry. Add jitter under sustained load. |
| 500 / 503 | Yes | Exponential backoff. Use idempotency-key so a retry of a write does not double-charge. |
| 501 | No | The endpoint is not live yet. |
For any state-changing call, set the lowercase idempotency-key header. The replay window is 24 h and the key is scoped to (developer, customer, key), so a retried topup, batch create or run kicks back the original result instead of a duplicate. One exception: a model: [...] cascade plus an idempotency-key is rejected with fallback_with_idempotency_unsupported.