Errors and handling
Understand connection errors, error events, stop reasons, fallback and spend limits, and retry safely.
Things go wrong in a few places: the connection, a single turn, a provider, or your spend limits. This guide shows what each looks like and what to do.
Connection errors
If the WebSocket cannot open, you get a normal HTTP status instead of an upgrade.
| Status | Meaning | What to do |
|---|---|---|
401 | Key missing, unknown or revoked | Check the key at vaktex.com/app |
404 | Reattach only: no such session for this key | Start a new session |
426 | The request was not a WebSocket upgrade | Use a WebSocket client |
503 | Key verification is briefly unavailable | Retry after a short wait |
Error events
During a connection, errors arrive as events and the socket stays open.
{ "type": "error", "code": "rate_limited", "message": "Monthly spend limit reached", "retryable": false, "seq": 3, "...": "..." } - An error that happens during a turn has a
seqand is followed byturn.complete. - An error about the connection itself, such as a malformed frame, has no
seq. retryablesays whether trying the same thing again might work.
The helper from the quickstart collects them in result.errors:
const result = await gateway.turn(request);
if (result.errors.length > 0) {
const { code, message, retryable } = result.errors[0];
console.error(`${code}: ${message}`);
} Error codes
The codes are the same for every provider, so you only handle one set.
| Code | Retryable | Meaning |
|---|---|---|
invalid_request | no | A frame or request failed validation. Fix the request |
unauthorized | no | Not authorized |
model_not_found | no | The provider and model pair does not exist |
no_capable_model | no | No model can serve this request, for example the images, tools or length do not fit |
refused | no | The models declined to answer |
session_busy | no | A turn is already running on this session |
session_not_found | no | No such session |
client_disconnect | no | The client went away |
provider_misconfigured | no | A provider is not set up correctly |
provider_unavailable | yes | A provider is down |
rate_limited | yes | A rate or spend limit was hit |
upstream_timeout | yes | The provider timed out |
upstream_error | yes | The provider returned an error |
interrupted | yes | The session restarted mid-turn. Send the turn again |
internal | no | Unexpected gateway error |
Spend limits use rate_limited but are marked not retryable, since waiting a few seconds will not help. See Spend limits.
Stop reasons
Check turn.complete.stop_reason before you trust a result. stop, length, stop_sequence and tool_use are normal endings. error, refused, rate_limited, disconnect and interrupted mean the answer may be missing or cut short. The full list is in Text inputs and outputs.
Fallback
When a model errors, is unavailable or refuses, the gateway can finish the turn on another capable model. It retries brief failures a couple of times first. If it switches, it tells you:
{ "type": "model.switched", "from": { "provider": "anthropic", "model": "claude-sonnet-5-5" }, "to": { "provider": "openai", "model": "gpt-6-sol" }, "reason": "error", "seq": 5, "...": "..." } reason is refusal, error or unavailable. Later events carry the new provider and model. Events you already received are never rewritten. If the first model had already written part of the answer, the next model continues from it.
turn.complete.attempts lists every model that was tried, in order, with an outcome:
| Outcome | Meaning |
|---|---|
completed | The model finished the turn |
retried | A brief failure, tried again on the same model |
error | The model failed |
refused | The model declined |
stopped | You stopped the turn, or a limit did |
A turn that has already delivered a tool call to you does not switch models. See Function calling.
If every candidate fails, you receive an error and the turn ends. When all of them refuse, the code is refused.
Spend limits
Your account can have limits on spend, measured in US dollars at model cost.
| Limit | What happens when you reach it |
|---|---|
| Monthly (calendar month, UTC) | Every running turn on the account stops |
| Weekly (Monday to Sunday, UTC) | Every running turn on the account stops |
| Per provider | Turns using that provider stop |
| 4 hours | New turns are blocked. Running turns finish |
A turn ended by a limit sends an error with code rate_limited and completes with stop_reason: "rate_limited". To stay ahead of limits, read budget on the stream: units_remaining is what is left for the tightest limit, and limit_window says which one.
If you have no weekly limit but do have a monthly one, the weekly limit defaults to a quarter of the monthly amount.
Retry safely
Retry only when retryable is true, wait between attempts, and give up after a few tries. A simple pattern is exponential backoff with a little randomness:
async function turnWithRetry(gateway, request, attempts = 4) {
for (let attempt = 0; ; attempt++) {
const result = await gateway.turn(request);
const error = result.errors[0];
if (!error || !error.retryable || attempt === attempts - 1) return result;
const delay = 500 * 2 ** attempt + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, delay));
}
} Two rules:
- Do not retry non-retryable errors.
invalid_requestfails the same way every time. - Do not repeat tool work. If a failed turn already delivered a tool call that you ran, send the result in the next turn instead of starting over.
Dropped connections
If the socket closes in the middle of a turn, reconnect and resume instead of starting over. See Reconnect and resume.
Check the service
GET https://gateway.vaktex.com/health returns { "healthy": true } and needs no key. /health/live and /health/ready are also available.
Get help
For anything not covered here, the full protocol reference is at gateway.vaktex.com/openapi.