Every error uses the same envelope.
json
{
"error": {
"code": 402,
"message": "Insufficient credits",
"metadata": {"error_type": "insufficient_credits"}
}
}code is the HTTP status as a number. Match on it, and on metadata.error_type — never on the message, which is written for a human and may be reworded.
Status codes
400invalid_request— malformed body or a bad parameter.401invalid_credentials— missing, unknown or revoked key.402insufficient_credits— balance at or below zero. Every request is refused, including free models: the block is on the account, not the model.403permission_denied— wrong key class. Usually an inference key on a management endpoint.404not_found— unknown model or route.413payload_too_large— longer than the context of every candidate.429rate_limited— see Limits.451region_not_served— we do not currently serve your region.502provider_error— the provider failed. The response carries their status and body.524provider_unreachable— no provider answered: unreachable, or silent past the deadline. Every candidate was tried.404no_endpoints_available— no endpoint satisfied the request. Names the filters that emptied the set.503service_unavailable— a feature this deployment has not configured, such as billing or BYOK key management.529provider_overloaded— the provider is at capacity.
Which of these are worth retrying
429, 502, 524 and 529 are transient — back off and retry. Failover already tries other providers before you ever see one.
402, 403, 413 and 503 are not. Retrying changes nothing, because the constraint is on your account or your request. 503 in particular is deliberately not a 502: it means every candidate was excluded by a rule you or your organisation set.