Guide
Error Codes
Every error of the Railwail API with its HTTP status, code, cause, fix and whether a retry helps. All errors share OpenAI's JSON format, so the OpenAI SDKs raise them as their usual exceptions.
- Body
- {"error": {message, type, code}}
- Codes
- 38 codes · 13 statuses
- Retry helps
- 429 limits, 5xx
- Never retry
- 400, 401, 402, 403, 404
Error Response Format
message is for humans, code is for your program: switch on code, never on the message. Chat completions add param (the field at fault, or null); strict-schema routes add details with the field errors.
{
"error": {
"message": "Invalid API key",
"type": "authentication_error",
"code": "invalid_api_key"
}
}{
"error": {
"message": "n > 1 is not supported; send one request per completion.",
"type": "invalid_request_error",
"param": "n",
"code": "invalid_request"
}
}{
"error": {
"type": "invalid_request_error",
"code": "validation_failed",
"message": "Request validation failed",
"details": {
"formErrors": ["Unrecognized key(s) in object: 'aspect_ratio'"],
"fieldErrors": {}
}
}
}See one for yourself, free: this call answers with the 401 above and uses no credits.
curl -s "https://railwail.com/api/v1/models?limit=1" \
-H "Authorization: Bearer rw_invalid"Not errors: image, embedding and video requests may answer 202 with status: "processing" and a Location: /api/v1/jobs/<id> header. Poll that job.
HTTP Status Codes
Filter by status, code or word. Every code has its own anchor, e.g. #trial_limit, so you can link straight to it.
- 400
The request body is not valid JSON.
Fix Send a JSON object with Content-Type: application/json.
No: fix first - 400
The body failed a check;
paramnames the field. Rejected on purpose: n > 1, logprobs / top_logprobs, audio output or non-text modalities, the deprecated functions / function_call, web_search_options, non-function tools, a tool_choice naming a function that is not in tools, role 'function', a tool message without tool_call_id.Fix Fix the field named in
param. One request per completion instead of n > 1; tools / tool_choice instead of functions.No: fix first - 400
Strict schema: an unknown or invalid field.
detailsholds the zod field errors (for example aspect_ratio or seed on images).Fix Remove or correct the fields listed in
details.No: fix first - 400
An image field (image_url, start_image or end_image) that the model has no input for. image_url and start_image are the same first frame.
Fix Send only the image fields the model's page lists, or pick an image-to-video model. Nothing was charged. → /docs/api/video-generation#request
No: fix first - 400
end_image was sent without image_url or start_image.
Fix Send the first frame too, or leave out end_image. Nothing was charged. → /docs/api/video-generation#request
No: fix first - invalid_value400unsupported_parameterinvalid_value
invalid_request_error
embeddings, audio/transcriptions
No: fix first
dimensions on a model that cannot be shortened (only text-embedding-3-small and -large can), or a value above the model's size (invalid_value). Transcriptions: a SeamlessM4T model without a language it supports (invalid_value).
Fix Leave out dimensions, or send at most 1,536 (-small) or 3,072 (-large). For SeamlessM4T send the language of the audio as ISO-639-1, e.g. en. → /docs/api/embeddings#request
No: fix first - 400
The provider refused an input: more tokens than the model accepts, an empty text, or too many inputs. The message gives the reason in our words.
Fix Split long texts or send fewer inputs per request. The run was refunded. → /docs/api/embeddings#errors
No: fix first - missing_fileempty_file400invalid_bodymissing_fileempty_file
invalid_request_error
audio/transcriptions, audio/uploads
No: fix first
The multipart body could not be read, has no
filepart, or the file is empty.Fix Send multipart/form-data with a non-empty
fileand amodelfield.No: fix first - invalid_provider400
A category or provider filter on GET /models that is not in the catalog's lists (until 25.09.2026 this answered 500).
paramnames the filter.Fix Use one of the values the message lists, or leave the filter out. → /docs/api/models
No: fix first - 400
A text, code or vision model that no chat provider serves (the Replicate-hosted ones, e.g. CodeLlama or Llama vision).
Fix Use a chat model from OpenAI, Anthropic, Google or DeepSeek, or run the model on its model page. → /docs/api/chat-completions#models
No: fix first - 400
A content part this provider cannot take, or one without a known price: DeepSeek accepts text only, Gemini needs images as base64 data URLs, files only as inline PDFs (file.file_data as a data URL, no file_id) with a readable page count, no audio parts (input_audio).
Fix Send text only, send the image or PDF as a data URL, transcribe audio first, or pick a model that takes it. → /docs/api/chat-completions#vision
No: fix first - 400
The model provider refused the request (HTTP 400, 413 or 422), for example a prompt above the context window or a schema the provider does not accept. Its message is passed through (up to 500 characters).
Fix Read the message, shorten the input or adjust the parameters.
No: fix first - 401
The key is missing, unknown, revoked or expired. The message is "Invalid API key".
Fix Send Authorization: Bearer rw_live_… with an active key. ↗ /dashboard/settings/api-keys
No: fix first - 402
The balance is lower than the credits this request holds up front (for chat: the input estimate plus max_tokens output tokens). Checked before any provider call.
Fix Top up your balance, or lower max_tokens. ↗ /dashboard/billing
After a top-up - 402
Your monthly spending limit would be exceeded by this request.
Fix Raise or remove the limit under Spend limits, or wait for the next month. ↗ /dashboard/settings/spend-limits
No: fix first - 402monthly_limit_exceeded
spending_limit_error
images, videos, embeddings, audio/speech, audio/transcriptions
No: fix first
The same monthly spending limit as spending_limit_reached, reported with a different code on these routes.
Fix Raise or remove the limit under Spend limits. ↗ /dashboard/settings/spend-limits
No: fix first - 403
The key lacks the scope of this route; the message names it. New keys get read + chat only, so images, video, audio and embeddings need their scope or "all".
Fix Create a new key with that scope or "all" (a key's scopes cannot be edited after creation). → /docs/authentication#scopes
No: fix first - 403
The key has an IP allowlist and this address is not on it. The message names the address the API saw.
Fix Add the address (or its CIDR block) to a new or rotated key, or call from an allowed host. → /docs/authentication#ip-allowlist
No: fix first - 403
The job belongs to another account.
Fix Poll only jobs created with a key of your account.
No: fix first - 404model_not_found
invalid_request_error
chat/completions, images, embeddings, videos, audio, models/{slug}
No: fix first
No active model has this slug (or provider model id). On chat/completions also for image, video and audio slugs: they are not chat models.
Fix Use a slug from the model pages or GET /api/v1/models. → /models
No: fix first - 404
Unknown job id. Job ids are UUIDs.
Fix Use the id from X-Railwail-Job-Id, the part after chatcmpl-, or job_id in a 202 answer.
No: fix first - 413
The uploaded file is larger than 25 MB (transcriptions; a request over 26 MB is refused before it is read) or 10 MB (voice samples on audio/uploads).
Fix Split or compress the audio.
No: fix first - type_mismatch415
The voice sample is not MP3, WAV, WebM or Ogg (checked on the file's bytes), or its declared type does not match its content.
Fix Export the clip as MP3 or WAV and upload it again. → /docs/api/audio-speech#voice-cloning
No: fix first - audio_unreadable400
The voice sample is longer than 60 seconds, or its length could not be read from the file.
Fix Trim the clip to at most 60 seconds (10-30 s of clear speech works best); if unreadable, export it as MP3 or WAV. → /docs/api/audio-speech#voice-cloning
No: fix first - 400
A voice_sample was sent without consent: true.
Fix Clone only your own voice or one you have permission to use, and confirm it with consent: true. Nothing was charged. → /docs/api/audio-speech#voice-cloning
No: fix first - 400
voice_sample is not the https url of your own upload from POST /api/v1/audio/uploads (other hosts, other accounts and image uploads are refused).
Fix Upload the clip with POST /api/v1/audio/uploads and pass the returned url within 24 hours. → /docs/api/audio-speech#voice-cloning
No: fix first - voice_sample_required400
voice_sample was sent to a model without a reference-voice input (message: model does not support voice cloning), or a model that only speaks in a cloned voice got no voice_sample.
Fix Pick a voice-cloning model for voice_sample, or send voice_sample and consent to a model that needs one. → /docs/api/audio-speech#voice-cloning
No: fix first - 413
The request would return more than 2,000,000 values (inputs × dimensions).
Fix Send fewer inputs per request, or ask for fewer dimensions. Nothing was charged. → /docs/api/embeddings#request
No: fix first - 429
The key's own requests-per-minute limit (60-6,000, default 600) is used up.
Fix Wait X-RateLimit-Reset seconds. There is no Retry-After header. → /docs/guides/rate-limits
After X-RateLimit-Reset - 429api_key_limit_exceeded
rate_limit_error
chat/completions, images, videos, audio/speech, audio/transcriptions, embeddings
After X-Key-Limit-Reset
The run would take the key past its own daily or monthly credit limit (set per key on the API keys page). Nothing was created or charged; for images with
n> 1, none of them. This is not the requests-per-minute limit.Fix Do not retry before the instant in
X-Key-Limit-Reset(ISO 8601, UTC: the next 00:00 UTC for the daily limit, the 1st of the next month for the monthly one); X-RateLimit-Reset does not apply. Or raise the key's limit under Limits & alerts. ↗ /dashboard/settings/api-keysAfter X-Key-Limit-Reset - 429
More than 20 voice sample uploads in one hour (per account, website and API together).
Fix Wait the Retry-After seconds; reuse an uploaded url for 24 hours instead of uploading the same clip again.
Yes, with backoff - 429
A trial rule for accounts without a purchase: runs over 2 credits, more than 5 runs in 24 h, 3 failures in a row, more than 8 trial runs per IP in 24 h, or an account younger than 24 h.
Fix Top up once to lift the trial rules, or lower max_tokens. ↗ /dashboard/billing
After a top-up - 429
The model provider is throttling requests.
Fix Retry with exponential backoff, or use another model.
Yes, with backoff - 499
The client closed the stream before the first chunk. Tokens generated until then are billed, the rest of the hold is refunded.
Fix Nothing to fix; your client ended the request.
n/a - 500
Unexpected server error.
Fix Retry once. If it persists, write to info@railwail.com with the time of the request (and for chat the X-Railwail-Job-Id).
Yes, with backoff - 502
The provider answered with a 5xx or the connection to it failed (embeddings: an answer without readable vectors). A failed run is refunded in full.
Fix Retry with exponential backoff.
Yes, with backoff - 504
The provider did not answer within 85 seconds. The run was stopped and refunded; there is no background job to poll.
Fix Retry, or send fewer inputs per request.
Yes, with backoff
38 of 38 codes
Retry policy
Retry with backoff
upstream_rate_limited, 502 upstream_error, 503 model_unavailable (or switch model), 500 internal_error once. Exponential backoff with jitter.Wait, then retry
rate_limit_exceeded: your key's own limit. Wait X-RateLimit-Reset seconds (there is no Retry-After). 429 api_key_limit_exceeded: the key's daily or monthly credit limit; not before the time in X-Key-Limit-Reset, or raise the limit. Rate limitsTop up first
insufficient_credits and 429 trial_limit (trial accounts: runs over 2 credits, 5 runs per 24 h, 3 failures in a row). BillingNever retry unchanged
spending_limit_reached: the same request fails again. Fix the request, key, scope or limit.The OpenAI SDKs already retry 429 and 5xx twice; that is harmless for the transient codes and useless for trial_limit and api_key_limit_exceeded (the refused attempts cost nothing). Over plain HTTP, a helper that follows the policy:
const RETRYABLE = new Set([
"rate_limit_exceeded", // your key's limit: wait X-RateLimit-Reset
"upstream_rate_limited", // the provider throttles
"upstream_error", // provider 5xx / connection
"model_unavailable", // may be temporary; try another model after a few attempts
"internal_error",
// not "api_key_limit_exceeded": the key's daily/monthly credit limit frees up only at X-Key-Limit-Reset
]);
async function chatWithRetry(body: object, tries = 4): Promise<any> {
for (let attempt = 1; ; attempt++) {
const res = await fetch("https://railwail.com/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RAILWAIL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await res.json();
// A slow non-stream run can fail after the 200 status was sent: check the body too.
if (res.ok && !data.error) return data;
const code: string = data.error?.code ?? `http_${res.status}`;
if (!RETRYABLE.has(code) || attempt >= tries) {
throw new Error(`${res.status} ${code}: ${data.error?.message ?? ""}`);
}
const reset = Number(res.headers.get("x-ratelimit-reset"));
const waitMs =
code === "rate_limit_exceeded" && reset > 0 ? reset * 1000 : 2 ** attempt * 500 + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
const data = await chatWithRetry({
model: "gpt-4o-mini",
max_tokens: 300,
messages: [{ role: "user", content: "Hello" }],
});
console.log(data.choices[0].message.content);import os
import random
import time
import requests
RETRYABLE = {
"rate_limit_exceeded", # your key's limit: wait X-RateLimit-Reset
"upstream_rate_limited", # the provider throttles
"upstream_error", # provider 5xx / connection
"model_unavailable", # may be temporary; try another model after a few attempts
"internal_error",
# not "api_key_limit_exceeded": the key's daily/monthly credit limit frees up only at X-Key-Limit-Reset
}
def chat_with_retry(body: dict, tries: int = 4) -> dict:
for attempt in range(1, tries + 1):
res = requests.post(
"https://railwail.com/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['RAILWAIL_API_KEY']}"},
json=body,
timeout=300,
)
data = res.json()
# A slow non-stream run can fail after the 200 status was sent: check the body too.
if res.ok and "error" not in data:
return data
error = data.get("error") or {}
code = error.get("code") or f"http_{res.status_code}"
if code not in RETRYABLE or attempt == tries:
raise RuntimeError(f"{res.status_code} {code}: {error.get('message', '')}")
reset = int(res.headers.get("x-ratelimit-reset") or 0)
time.sleep(reset if code == "rate_limit_exceeded" and reset > 0 else 2 ** attempt * 0.5 + random.random() / 4)
data = chat_with_retry({
"model": "gpt-4o-mini",
"max_tokens": 300,
"messages": [{"role": "user", "content": "Hello"}],
})
print(data["choices"][0]["message"]["content"])Errors in streams and slow answers
- Before the first byte of a stream: a normal HTTP error status and body, and the hold is refunded in full.
- After the first byte: an SSE event
data: {"error": {…}}withoutdata: [DONE]. The OpenAI SDKs raise it asAPIError. - You close the stream: tokens generated so far are billed, the rest of the hold is refunded; before the first chunk the status is 499
client_closed_request. - Non-stream answers slower than 25 s: the server keeps the connection alive with whitespace, so the status is already 200; a later failure arrives as an
{"error": …}body with HTTP 200. Check the body, or usestream: true.
What an error costs
Errors cost nothing. A request refused before the run reserves nothing; a run that fails at the provider (400 provider_rejected_request, 429 upstream_rate_limited, 5xx, an error inside a stream) is refunded in full. Billed are only a stream you close yourself (for the tokens generated until then), a non-stream request whose connection you drop (it runs to the end), and a GPU run stopped at its time limit. Holds and refunds show in Billing.
Known differences between routes
The routes are not fully uniform yet. Until they are, handle these as they are:
- Monthly spending limit:
spending_limit_reached(typeinsufficient_credits_error) on chat,monthly_limit_exceeded(typespending_limit_error) on videos and audio, and 500internal_erroron images and embeddings. GET /api/v1/modelsanswers 500 for an invalidcategoryorproviderfilter instead of 400.GET /api/v1/jobs/{id}answers another account's job with 403forbiddenof typeauthentication_error.- Rate-limit headers are missing on 202 answers of images and embeddings, and on
GET /modelsandGET /jobs.
Error Handling in Code
With the railwail npm SDK every JSON error becomes a RailwailError; timeouts, network failures and non-JSON bodies throw their own errors. With the OpenAI SDKs, APIError (TypeScript) or APIStatusError (Python) carries the same code.
import railwail, { RailwailError } from "railwail";
const rw = railwail(process.env.RAILWAIL_API_KEY!);
try {
await rw.chat("gpt-4o-mini", [{ role: "user", content: "Hello" }], { max_tokens: 300 });
} catch (err) {
if (err instanceof RailwailError) {
console.error(err.status, err.type, err.code, err.message);
} else {
throw err; // AbortError (timeout), TypeError (network), SyntaxError (non-JSON body)
}
}