Errors & Limits
Every error is a flat JSON object with an error_code and a human-readable message. Nothing is ever silently dropped — a rejected field or exhausted limit always comes back as one of the bodies below.
401 — missing or invalid key
{
"error_code": "UNAUTHORIZED",
"message": "Invalid or revoked API key",
"retryable": false
}402 — insufficient credits
Returned when the account balance can't cover the request.
{
"code": "INSUFFICIENT_CREDITS",
"message": "You've run out of credits. Purchase more to continue using cloud models.",
"available_microcredits": 0,
"required_microcredits": 5250,
"available_credits_display": "0.00",
"required_credits_display": "5.25",
"purchase_url": "https://verdictide.com/account/billing/credits?..."
}403 — forbidden
Two distinct cases share this status code:
A test-environment key on a live endpoint — there is no test environment at launch:
{
"error_code": "test_key_not_allowed",
"message": "Test-environment API keys are not accepted on /v1/chat/completions.",
"retryable": false
}A key missing the required scope:
{
"error_code": "insufficient_scope",
"message": "This API key does not have the 'llm:chat' scope.",
"retryable": false
}429 — rate limited
{
"error_code": "rate_limit_exceeded",
"message": "Per-key rate limit exceeded: 60 requests/minute.",
"retryable": true
}Carries a Retry-After header (seconds until the current window resets).
400 — unsupported parameter
Returned for a rejected field per the compatibility matrix — the response names the field.
{
"error_code": "unsupported_parameter",
"message": "Parameter 'seed' is not supported.",
"retryable": false,
"param": "seed"
}x-ratelimit-* headers
Sent on every response from a key that has a configured limit. An unlimited key emits none of these — there is no ceiling to report.
| Header | Meaning |
|---|---|
x-ratelimit-limit-requests | The configured ceiling for the primary window (per-minute if rpm is set, otherwise per-day). |
x-ratelimit-remaining-requests | Requests left in the current primary window. |
x-ratelimit-reset-requests | Time until the primary window resets, formatted like 21s or 1m0s. |
x-ratelimit-limit-requests-day / -remaining-requests-day / -reset-requests-day | The daily ceiling, only emitted when both a per-minute and a per-day limit are configured on the same key. |
Retry-After | Seconds to wait before retrying, sent only on a 429. |
Rate limiting is per-instance
At launch, rpm/rpd limits are enforced per Gateway instance, approximately— not with a single shared counter. With N instances behind the load balancer, the effective ceiling for a key is up to N× its configured value. This is a documented launch posture, not a bug; a shared atomic counter is the upgrade path once the API tier carries real traffic.
Key revocation
A revoked or rotated key stops authenticating within 60 seconds or less, everywhere — each Gateway instance caches key lookups for up to 60 seconds before re-checking.
Key expiration
Every key can be given an expiration when it's created: 30, 90, or 365 days, or never. The console defaults new keys to 90 days. An expired key fails the same way a revoked one does.
Scopes
A key needs the llm:chat scope to call /v1/chat/completions. A key missing it gets the 403 insufficient_scope body above rather than a silent failure.