What do VARTA API errors look like and how do I handle them?
Every error from /v1 — regardless of what went wrong inside the platform — comes back
as the same JSON shape, with an HTTP status that matches it, with one exception:
unauthenticated can also arrive as a bare
{"detail": "authentication required"} with no envelope at all, rejected before a /v1
handler ever runs — see that page for when.
{
"error": {
"type": "invalid_request_error",
"code": "workflow_not_found",
"message": "No agent with id 'ag_x' exists for this tenant.",
"param": "agent_id",
"request_id": "req_a91c4e7b2f30",
"doc_url": "https://vartavoice.ai/docs/errors/workflow_not_found"
}
}typeis the coarse bucket a client branches on —invalid_request_error,authentication_error,permission_error,billing_error,rate_limit_error,provider_error, orapi_error.codeis the stable contract.messageis for humans and may be reworded — write your error handling againstcode, never against the text ofmessage.paramnames the offending request field, when the error is about one specific field. It'snullotherwise.request_idis unique per request. Quote it when you contact support about a 5xx.doc_urlpoints athttps://vartavoice.ai/docs/errors/<code>— the value in the JSON example above is what the platform emits (DOC_BASEinbackend/app/api/v1/errors.py), and it is the page for that code on this site: the same one the table's links below open. It is safe to follow, and safe to use as a stable identifier for the code.
Unrecognised errors still parse
Only the codes in the table below are permanent. If the platform hits an internal
failure it hasn't given a specific code, it degrades to one of a small set of
per-status fallback codes (for example, an unrecognised 404 becomes not_found, an
unrecognised 429 becomes rate_limit_exceeded) rather than breaking the envelope
shape. Two statuses have no fallback entry at all and so land on internal_error
while keeping their own status: a 405 (wrong HTTP method for a /v1 path) and a
415 (unsupported media type) both arrive as code: "internal_error" with status
405 / 415 respectively (backend/app/api/v1/errors.py:99-111) — so an
internal_error on a non-5xx status is a malformed request, not a platform fault,
and retrying it unchanged will not help. So it's safe to build a client that switches on type and a known subset of
codes, and falls through to message for anything else — you will never get back
something that isn't this envelope.
All error codes
| Code | HTTP status | Type |
|---|---|---|
invalid_request |
400 | invalid_request_error |
validation_error |
422 | invalid_request_error |
unauthenticated |
401 | authentication_error |
invalid_api_key |
401 | authentication_error |
token_expired |
401 | authentication_error |
insufficient_credits |
402 | billing_error |
insufficient_role |
403 | permission_error |
insufficient_scope |
403 | permission_error |
tenant_forbidden |
403 | permission_error |
destination_not_permitted |
403 | permission_error |
not_found |
404 | invalid_request_error |
workflow_not_found |
404 | invalid_request_error |
session_not_found |
404 | invalid_request_error |
campaign_not_found |
404 | invalid_request_error |
step_not_found |
404 | invalid_request_error |
trunk_not_found |
404 | invalid_request_error |
session_already_ended |
409 | invalid_request_error |
idempotency_key_reuse |
409 | invalid_request_error |
idempotency_key_in_progress |
409 | invalid_request_error |
caller_id_not_on_trunk |
400 | invalid_request_error |
dial_failed |
502 | provider_error |
telephony_not_configured |
503 | provider_error |
rate_limit_exceeded |
429 | rate_limit_error |
concurrency_limit_exceeded |
429 | rate_limit_error |
internal_error |
500 | api_error |
provider_error |
502 | provider_error |
provider_unavailable |
503 | provider_error |
provider_timeout |
504 | provider_error |
Each page answers three questions: what the code means, what causes it in this platform, and what to do about it. Where the underlying code has no live path that raises it yet, the page says so directly rather than guessing.