Errors and limits
GetVibe API error codes: 401 (key), 402 (balance), 403 (IP), 429 (rate limit). How to handle errors and retry with exponential backoff.
Errors produced by GetVibe on /v1/* have a JSON envelope with error.message, error.type and error.code. Do not rely on the message text or third-party service headers as a contract.
Error format
{
"error": {
"message": "insufficient balance",
"type": "insufficient_quota",
"code": 402,
"request_id": "8d4b6b3a-..."
}
}Response codes
| Parameter | Type | Description |
|---|---|---|
401 | authentication_error / 401 | Missing or malformed key, or the key is disabled. Do not retry until the credentials are fixed. |
402 | insufficient_quota / 402 | No available balance or limit — waiting does not fix it. A 402 never carries Retry-After and should not be retried: top up, or wait for the subscription window to reset. |
403 | permission_error or policy type / 403 | IP, model or policy does not allow the request. Do not retry without changing the request or the settings. |
429 | rate_limit_exceeded, inflight_client_cap or rate_limit_error / 429 | Your rate limit, your concurrency limit, a balance reservation briefly held by your own concurrent requests (cause reserved_in_flight), or a supplier limit. Retry only after Retry-After; a 429 never means your balance is exhausted. |
410 | media_endpoint_retired / media_endpoint_retired | Only /v1/images/edits, /v1/video/generations and /v1/videos (retired). Do not retry; switch to /api/media/generate. /v1/images/generations is served. |
413 | invalid_request_error / 413 | Files API only: the file exceeds 50 MiB. Reduce the file, then create a new request. |
503 | overloaded / 503 | Global edge overload. Retry after Retry-After: 2. Other 503s may not be retryable without changing the model or request. |
Request errors (400, 413, 422): codes and fixes
Every request error carries a stable error.code from the table below, error.param with the field it concerns (null when the model did not name one), and a link to the matching row at the end of error.message — Codex and other CLIs show only the message, so the link travels there.
| error.code | error.param | What to do |
|---|---|---|
unsupported_parameter | field name | The selected model or channel does not accept this parameter. Remove it from the request (in Codex/IDEs, from the config) and retry. |
invalid_parameter_value | field name | The parameter is present but its value is not allowed for this model: for example temperature other than 1 with reasoning on, max_tokens above the maximum, or effort "none" where reasoning cannot be turned off. Fix the value as the message says. |
missing_parameter | field name | A required field is missing or empty. Add it. |
invalid_tool_schema | tools[].function.* | A tool definition was rejected: names may use Latin letters, digits, "_" and "-" only; parameters must be a JSON Schema whose root is type: "object". Or pick another variant of the model. |
tool_choice_unsupported | tool_choice | The model does not support a forced tool choice. Send tool_choice "auto" or "none". |
invalid_tool_history | messages / input | The history contains a tool call without a result. Add the result with the same id, or drop the call together with its result when trimming history. |
invalid_image | — | The image could not be read. Send a complete PNG, JPEG, WebP or GIF file (BMP, TIFF, AVIF and SVG are not accepted). |
unsupported_input_modality | — | The model does not accept this kind of attachment (PDF, audio, video, images). Remove it or pick a model whose catalog entry lists that input. |
conversation_state_unreadable | — | The reasoning in the history was created by another channel. The request is valid; retry it, and if the error repeats, start a new conversation. |
invalid_json | — | The request body is not JSON or arrived truncated. For very long conversations with images, start a new conversation or compact the history. |
request_too_large | — | The request is too large. Reduce the attachments or the history. |
request_rejected_by_channel | — | The model channel refused without naming a reason; the request itself is most likely valid. Retry it; if the refusal repeats, pick another variant of the model (the suffix after the colon) or the model without a suffix. If that does not help, contact support with the request_id. |
model_not_available_on_route | — | The route is not set up for this model. Pick another model or variant. |
context_length_exceeded | messages / input / prompt | The request does not fit the context window. Compact the history (in Codex: /compact) or pick a model with a larger window. |
content_moderation | — | The request or the answer was blocked by the provider's content filter. Rephrase it or pick another model. |
invalid_request | — | The cause was not recognised. Check the request against the API reference; if it is correct, contact support with the request_id from the response. |
Response headers
| Parameter | Type | Description |
|---|---|---|
x-pv-cost-rub | string | Best-effort ruble cost estimate. In streams, x-pv-cost-rub is available when the input estimate is known; pv_cost_rub in the final usage event requires x-pv-cost: 1. The usage history contains the actual debit. |
x-pv-generation-id | string | Usage-history record id (GET /v1/generations/{id}). Returned only for non-streaming chat/completions requests. |
x-pv-dropped-params | string | Comma-separated request parameters that the selected route does not accept and that were therefore not sent to the model (for example temperature,top_p). Present only when something was actually withheld, on answers and on errors alike. We never drop a parameter silently: no header means everything you sent reached the model unchanged. |
Retry-After | integer | Seconds to wait. The edge sets it on 429 rate, 429 concurrency (5), 503 global overload (2) and some transient 402s. Not assumed for every 429/503. |
Errors before and after the response starts
A slow stream (stream: true) gets 200 and : pv-keepalive comments after 5 seconds of silence so the connection does not look dead; a long non-stream request gets 200 and whitespace after its first token. From then on the status cannot change and an error arrives in the body. If your client falls back to another supplier on an HTTP error, send x-pv-honest-errors: 1 (or ask support to enable it for the account): there is then no keepalive and no early 200 before the first response byte, and every failure is an ordinary HTTP status with a JSON body. The cost is a silent socket until the first token; your header timeout must cover it.
An error after the response has started is one terminal event: for chat event: error with {"error":{message,type,code,status},"choices":[{"finish_reason":"error"}]} followed by data: [DONE]; for /v1/responses event: response.failed carrying the same error; for /v1/messages Anthropic's event: error. code is the machine cause and status is the HTTP status the error would have had before the response started.
stream: true) x-pv-generation-id is not returned because billing finishes after the stream ends. Review the request history via GET /v1/generations.Rate and concurrency limits
Both limits depend on the account's current balance and apply to all of its API keys together. The tier is recalculated automatically: top up and the limit rises; drop below a threshold and the tier goes down. There is no tokens-per-minute (TPM) limit.
| Balance from | Requests per minute (RPM) | Concurrent requests |
|---|---|---|
| 0 $ | 300 | 10 |
| 1 $ | 1 100 | 50 |
| 2 $ | 6 600 | 300 |
| 4 $ | 19 800 | 900 |
| 12 $ | 44 000 | 2 000 |
| Parameter | Type | Description |
|---|---|---|
429 rate_limit_exceeded | RPM | The account sent as many requests in the last 60 seconds (a sliding window) as its tier allows. Retry-After is the number of seconds until a slot in the window frees up (at least 1); x-ratelimit-limit-requests is your current RPM. Body: error.cause = key_rate_limited, error.details = { enforced_by: "getvibe", limit, unit: "requests_per_minute", retry_after_seconds }. |
429 inflight_client_cap | concurrency | As many requests are already running as the tier allows. A request holds its slot until it finishes; a stream holds it until its last event. Retry-After: 5. Body: error.cause = account_concurrency_limited, error.details = { enforced_by: "getvibe", limit, unit: "parallel_requests", retry_after_seconds: 5 }. |
Personal limit | RPM | If the account has a personal request limit, it can only lower the tier RPM, never raise it. |
503 overloaded | service | Not your limit but service protection: Retry-After: 2 is the global cap across all accounts, Retry-After: 1 means one server is busy. Retry after the pause. |
429 from a provider | upstream | error.cause = supplier_rate_limited means the model provider throttled the request and your own limit is not exhausted; channel_rate_limited is the shared cap of a provider channel (Retry-After: 10, nothing was sent). |
Retrying requests
For a retryable response first use Retry-After, if present. Without the header, do not treat any 429 or 503 as automatically retryable: check the HTTP status and the public error.type, fix the non-retryable cause and use bounded backoff only for a transient error.
import time, requests
def call_with_retry(url, headers, payload, max_retries=5):
for attempt in range(max_retries):
r = requests.post(url, headers=headers, json=payload)
if r.status_code == 429:
# honor Retry-After, otherwise exponential backoff
wait = int(r.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
continue
return r
return r