Skip to content

Errors

Body format

Error responses do not all share one envelope. There are three shapes:

1. Domain errors (thrown through onError) use the standard envelope:

json
{ "error": { "code": "invalid_credentials", "message": "Invalid credentials" } }

code is a stable machine-readable string; message is human-readable and may change.

2. 400 validation errors (Hono's Zod validation) use the validator's own shape, with no code:

json
{ "success": false, "error": { "issues": [  ], "name": "ZodError" } }

3. Some 401/403 responses (webhook signature, api-key ownership) return a flat string, not an object:

json
{ "error": "Missing signature" }
json
{ "error": "Forbidden" }

Recommendation: match primarily on the HTTP status code; treat code as secondary — it is absent for validation errors and flat-string responses, and message may change.

Status code table

StatuscodeRaised whenHow to handle
400— (Zod format)Validation failed (body/schema)Fix the request payload; check required fields
401invalid_credentialsLogin with wrong email/passwordPrompt for credentials again
401unauthorizedMissing/invalid session, session revoked, customer suspended, invalid tokenRe-authenticate (log in again)
401errorMissing/invalid/revoked API keyCheck the key header
401— (plain string)Missing/invalid webhook signature ({"error":"Missing signature"} / {"error":"Invalid signature"})Check the X-YooKassa-Signature header
402insufficient_balanceReservation/debit/adjustment would go negativeTop up the balance and retry
403forbidden (reserved)ForbiddenError is defined in the error map but not thrown in beta
403errorOwnership/role mismatch on admin routes, CSRF (Invalid origin / Missing origin), insufficient scope, workspace/customer suspendedConfirm you own the resource; send a valid Origin; grant the required scope
403— (plain string)API-key ownership mismatch ({"error":"Forbidden"})Confirm the key belongs to the workspace
404not_foundUnknown model, unknown workspace, unknown API key, no pricing for modelCheck the identifier
409email_already_registeredRegister with an existing emailLog in instead
429quota_exceeded (reserved)Quota limit hit — reserved: not returned in beta (quota is enforced by atomically skipping the increment, not by rejecting the request)Reduce usage or raise the quota
429errorRate limit exceeded (per-key or auth)Back off and retry after X-RateLimit-Reset
500internal_errorUnhandled server errorRetry with backoff; report

Reserved codes. quota_exceeded and forbidden exist in the error map but are not returned in beta. Quotas are enforced atomically (the usage increment is skipped past quota, so the request does not fail with 429). 403 in practice comes as an error-code envelope (ownership/CSRF/scope) or a flat {"error":"Forbidden"} string (api-key ownership).

Handling guidance

  • Retry only idempotent requests. Webhooks are idempotent by design; chat completions are not safe to blindly replay (each replay is billed).
  • 401 on a session means the cookie is gone, revoked, or expired — re-login, don't loop.
  • 402 is a balance problem, not a bug: it means the reservation or deficit could not be covered.
  • 403 insufficient scope is returned as a plain error code with a message like Insufficient scope: chat:write required — check the key's scopes.
  • 429 carries rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) on the per-key chat path — read them before retrying.

Auth vs. rate-limit specifics

  • Auth rate limiting applies to POST /v1/auth/login (10/min/IP) and POST /v1/auth/register (5/min/IP), returning 429 with code error (Too many attempts).
  • Per-key rate limiting applies to POST /v1/chat/completions, returning 429 with code error (Rate limit exceeded).

See Rate Limits & Quotas for details.