Erori
Există două vocabulare de erori în CallAgent, iar confundarea lor este cea mai frecventă greșeală de integrare. Citește această secțiune înainte de tabele.
error.type | error.code | |
|---|---|---|
| Unde | Orice răspuns HTTP non-2xx de la /api/v1 | Eșecuri de provider, evenimente de apel, respingeri de tool |
| Răspunde la | „ce fel de eșec a fost acest request?" | „ce anume a mers prost în pipeline-ul de voce?" |
| Dimensiune | 10 valori din rendererul central, plus câteva locale | 55 de valori, comune pe toată platforma |
| Mereu prezent | Da, pe o eroare randată | Nu — doar pe erorile care au unul |
Dacă scrii un client HTTP, ramifică pe error.type și pe codul de status.error.code este un detaliu în plus atunci când există, și lipsește frecvent. Dacă tratezi un eveniment de apel sau un payload de webhook, error.code este cel de care ai nevoie.
Forma erorii HTTP
{
"error": {
"type": "validation_error",
"message": "The request payload failed validation.",
"errors": { "name": ["The name field is required."] },
"documentation_url": "https://docs.callagent.dev/api/errors#validation",
"request_id": "01JQ8Z9K3T4V5W6X7Y8Z9A0B1C"
}
}type și message sunt mereu prezente. errors, code, retryable, provider, provider_request_id, limit, used și request_id apar acolo unde se aplică.
request_id nu apare pe fiecare răspuns
Apare pe erorile randate de handlerul central. Nu apare pe niciun 2xx și nu apare pe erorile ridicate de middleware-ul de autentificare și de câteva controllere care își construiesc propriul body — inclusiv pe o cheie API greșită, cea mai frecventă eroare pe care o vei vedea. Niciun răspuns nu poartă nici header-ul X-Request-Id.
Poți furniza tu unul: trimite X-Request-Id pe request și eroarea randată ți-l va întoarce, trunchiat la 64 de caractere. Aceasta este modalitatea sigură de a corela un eșec cu propriile tale log-uri.
Tipurile
type | Status | Ce înseamnă |
|---|---|---|
authentication_error | 401 403 | Cheie lipsă, invalidă, revocată sau expirată; sau o cheie nepermisă de la acest origin ori de la această adresă. |
organization_error | 403 | Organizația este suspendată sau altfel inactivă. |
permission_error | 403 | Autentificat, dar cheia nu poartă scope-ul necesar. Mesajul numește scope-ul. |
not_found | 404 | Nu există o astfel de resursă — sau aparține altei organizații. Cele două sunt deliberat imposibil de deosebit. |
validation_error | 422 | Body-ul a picat validarea. errors este o mapă indexată pe câmpuri. |
plan_limit_exceeded | 402 | Un plafon de plan. code numește entitlement-ul, cu limit și used. |
rate_limit_error | 429 | Prea multe request-uri. retryable: true, plus Retry-After. |
provider_error | 502 504 429 | Un provider din amonte a eșuat. Poartă provider, code, retryable și adesea provider_request_id. |
ssrf_blocked | 422 | Un URL furnizat de tine s-a rezolvat undeva unde nu are voie să ajungă. code numește motivul. |
http_error | variază | O eroare HTTP fără o mapare mai specifică. |
server_error | 500 | Vina noastră. Deja raportată la noi. request_id este ce îi trebuie support-ului. |
Un 500 nu întoarce niciodată mesajul excepției. message este întotdeauna Something went wrong on our side. The incident has been recorded.
Tipuri ridicate local de unele handlere
Acestea ocolesc rendererul central, așa că nu poartă request_id și uneori nici documentation_url. Tratează-le la fel ca pe echivalentul de mai sus.
type | Status | Unde |
|---|---|---|
retrieval_error | 422 | Interogare în knowledge base |
invalid_integration | 422 | Conectarea unei integrări |
invalid_state | 422 | Replay al livrării unui webhook — atenție, numele sugerează 409, dar este 422 |
forbidden | 403 | Verificări de permisiuni în contact centre |
billing_unavailable | 503 | Stripe nu este configurat pe acest deployment |
telephony_error | 409 422 502 | Eșecuri de carrier și de provizionare. Un envelope diferit: {type, message, provider, code, retryable}, fără documentation_url și fără request_id. |
not_implemented | 501 | Un provider de telefonie fără adapter pentru această capabilitate |
Tratarea erorilor HTTP
Ramifică întâi pe status, apoi pe tip. Nu deduce din codul de status dacă se poate face retry — ai încredere în retryable atunci când este prezent.
async function call(url, init) {
const requestId = crypto.randomUUID();
const response = await fetch(url, {
...init,
headers: { ...init.headers, 'X-Request-Id': requestId },
});
if (response.ok) return response.json();
const { error } = await response.json();
switch (response.status) {
case 401:
case 403:
// Fără retry. Repară cheia, scope-urile ei sau restricția de origin.
throw new Error(`${error.type}: ${error.message}`);
case 422:
// error.errors este câmp -> [mesaje] când type este validation_error.
throw new Error(`${error.message} ${JSON.stringify(error.errors ?? {})}`);
case 402:
// Plafon de plan: error.code numește entitlement-ul, cu limit și used.
throw new Error(`${error.code}: ${error.used}/${error.limit}`);
case 429:
return retryAfter(Number(response.headers.get('Retry-After') ?? 1));
default:
if (error.retryable) return retryAfter(5);
// Dă-i support-ului request_id; dacă lipsește, folosește-l pe cel trimis de noi.
throw new Error(`${error.type}: ${error.message} (${error.request_id ?? requestId})`);
}
}<?php
function handle(int $status, array $body, array $headers, string $requestId): never
{
$error = $body['error'] ?? ['type' => 'unknown', 'message' => 'No error body.'];
// Fără retry: repară cheia, scope-urile ei sau restricția de origin.
if ($status === 401 || $status === 403) {
throw new RuntimeException("{$error['type']}: {$error['message']}");
}
if ($status === 422) {
throw new InvalidArgumentException(
$error['message'] . ' ' . json_encode($error['errors'] ?? []),
);
}
// Plafon de plan: `code` numește entitlement-ul, cu `limit` și `used`.
if ($status === 402) {
throw new RuntimeException("{$error['code']}: {$error['used']}/{$error['limit']}");
}
if ($status === 429) {
throw new RetryLater((int) ($headers['retry-after'] ?? 1));
}
if (($error['retryable'] ?? false) === true) {
throw new RetryLater(5);
}
// Dă-i support-ului request_id; dacă lipsește, folosește-l pe cel trimis de noi.
throw new RuntimeException(
"{$error['type']}: {$error['message']} (" . ($error['request_id'] ?? $requestId) . ')',
);
}# -i ca să vezi statusul și header-ele de rate limit alături de body.
curl -i -X POST "$CALLAGENT_API_URL/assistants" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: $(uuidgen)" \
-d '{"name": "Support Line"}'
# Cele două câmpuri care merită citite la orice eșec:
# .error.type ce fel de eșec a fost
# .error.request_id ce va cere support-ulVocabularul comun error.code
55 de coduri, definite o singură dată ca CallAgentErrorCode în @callagent/contracts și folosite identic în răspunsurile HTTP, în evenimentele de apel și în payload-urile de webhook — astfel încât provider_timeout înseamnă același lucru oriunde îl întâlnești.
Retryable
Zece coduri sunt retryable, iar retryable dintr-un răspuns este derivat din această listă, nu setat de mână:
provider_timeout · provider_rate_limit · provider_unavailable · provider_bad_response · mcp_connection_failed · mcp_timeout · tool_timeout · storage_failure · media_failure · rate_limited
Tot restul eșuează identic la a doua încercare. Fă retry pentru cele zece; repară-le pe celelalte.
Provideri
| Cod | HTTP | Semnificație |
|---|---|---|
provider_timeout | 504 | Providerul nu a răspuns la timp. |
provider_auth | 502 | Providerul a respins credențialul. |
provider_rate_limit | 429 | Providerul ne aplică throttling. |
provider_unavailable | 503 | Providerul este picat sau inaccesibil. |
provider_bad_response | 500 | Providerul a răspuns cu ceva inutilizabil. |
provider_quota_exceeded | 402 | Cota contului la provider este consumată. |
provider_unsupported | 500 | Providerul nu suportă ce i s-a cerut. |
all_providers_failed | 503 | Providerul principal și toate cele de rezervă au eșuat. |
circuit_open | 503 | Breakerul este deschis; providerul este lăsat să se odihnească. |
Siguranță outbound
| Cod | HTTP | Semnificație |
|---|---|---|
ssrf_blocked | 403 | Un URL furnizat de tenant s-a rezolvat undeva unde nu are voie să ajungă. |
sandbox_denied | 403 | Un tool rulat în sandbox a cerut ceva ce sandbox-ul refuză. |
sandbox_timeout | 504 | Un tool rulat în sandbox a depășit limita de timp. |
response_too_large | 500 | Răspunsul unui tool a depășit plafonul de dimensiune. |
ssrf_blocked este 403 aici și 422 peste HTTP
Vocabularul comun mapează ssrf_blocked la 403; rendererul HTTP al control plane-ului returnează 422 pentru același eșec, pe motiv că URL-ul trimis de tine este un request greșit, nu unul interzis. Peste HTTP, așteaptă-te la 422 cu error.type: "ssrf_blocked".
Tools și MCP
| Cod | HTTP | Semnificație |
|---|---|---|
tool_rejected | 500 | Apelul a fost refuzat — vezi motivul respingerii. |
tool_timeout | 504 | Tool-ul a depășit timeoutSeconds. |
tool_failed | 500 | Tool-ul a rulat și a eșuat. |
tool_not_found | 404 | Nu există un astfel de tool pe acest assistant. |
invalid_tool_arguments | 422 | Argumentele modelului nu s-au potrivit cu schema de parametri. |
mcp_unauthorized | 502 | Serverul MCP a respins credențialul. |
mcp_connection_failed | 500 | Serverul MCP nu a putut fi contactat. |
mcp_tool_not_found | 404 | Tool-ul numit nu se află pe acel server. |
mcp_timeout | 504 | Apelul MCP și-a depășit timeout-ul. |
mcp_permission_denied | 403 | Politica sau nivelul de risc a blocat apelul. |
Knowledge
| Cod | HTTP | Semnificație |
|---|---|---|
knowledge_unavailable | 500 | Retrieval-ul a eșuat. |
knowledge_insufficient | 500 | Nu a venit înapoi nimic relevant, iar assistantul este în mod knowledge-only. |
Admisie
Verificate înainte ca un apel să fie creat, deci acestea apar pe un request de apel, nu în mijlocul conversației.
| Cod | HTTP | Semnificație |
|---|---|---|
concurrency_exceeded | 429 | Nu mai există spațiu de concurență. |
insufficient_credits | 402 | Portofelul nu poate finanța apelul. |
organization_suspended | 403 | Organizația nu are voie să inițieze apeluri. |
destination_blocked | 403 | Politica de destinații a refuzat numărul. |
rate_limited | 429 | Prea multe request-uri. |
POST /calls returnează 201 chiar și când originarea eșuează
Serviciul de apeluri outbound prinde eșecurile de carrier și de admisie, marchează apelul ca eșuat și răspunde totuși 201. O respingere pe concurență, o destinație blocată și o pană de carrier arată toate ca „creat" dacă te uiți doar la codul de status. Citește status și ended_reason din body-ul răspunsului, nu codul de status, ca să știi dacă apelul chiar va avea loc.
Telefonie
| Cod | HTTP | Semnificație |
|---|---|---|
telephony_rejected | 500 | Carrierul a refuzat apelul. |
sip_error | 500 | Un eșec la nivel SIP; codul de răspuns se află pe eveniment. |
transfer_failed | 500 | Transferul nu s-a finalizat. |
no_answer | 500 | Nu a răspuns nimeni. |
media_failure | 500 | Audio-ul a încetat să circule. |
Contact centre
| Cod | HTTP | Semnificație |
|---|---|---|
no_operator_available | 503 | Niciun operator nu a putut prelua apelul. |
queue_full | 503 | Coada este la max_waiting_calls. |
Stocare
| Cod | HTTP | Semnificație |
|---|---|---|
storage_failure | 500 | Scrierea sau citirea unui artifact a eșuat. |
recording_unavailable | 500 | Înregistrarea nu există sau a fost ștearsă. |
Configurare și lookup
| Cod | HTTP | Semnificație |
|---|---|---|
invalid_configuration | 422 | Configurația assistantului, a tool-ului sau a trunk-ului nu este validă. |
assistant_not_found | 404 | |
phone_number_not_found | 404 | |
call_not_found | 404 | |
invalid_state_transition | 409 | Ceva a cerut unui apel să treacă într-o stare la care nu poate ajunge. |
Transport și platformă
| Cod | HTTP | Semnificație |
|---|---|---|
unauthorized | 401 | Cheie lipsă sau invalidă. |
forbidden | 403 | Autentificat, dar fără permisiune — de obicei un scope lipsă. |
not_found | 404 | |
validation_failed | 422 | Body-ul request-ului a picat validarea; vezi errors. |
idempotency_conflict | 409 | Aceeași cheie de idempotență a fost folosită pentru un alt request. |
aborted | 500 | Operațiunea a fost anulată în timpul execuției. |
internal_error | 500 | Varianta de rezervă. Dacă vezi asta, request_id este ce îi trebuie support-ului. |
Motive de respingere a unui tool
Un apel de tool respins poartă un rejectionReason dintr-un vocabular separat, mai restrâns. Acesta este ce i se comunică modelului, ca să poată ocoli refuzul în loc să reîncerce:
user_rejected · forbidden_tool · permission_denied · confirmation_required · policy_blocked · ssrf_blocked · rate_limited · invalid_arguments · not_found · timeout · sandbox_denied
Vezi Tools.
Unde duce documentation_url
Erorile randate poartă un documentation_url. Nu-l urma.
Fiecare este un URL absolut către https://docs.callagent.dev, hard-codat la locul unde este ridicat în control plane — paisprezece literale de string separate, nu o singură valoare de configurare. Acel hostname nu este acest site și nu este servit de acest deployment, deci link-ul nu poate fi salvat printr-un redirect de aici: un client care îl urmează nu ajunge niciodată la origin-ul nostru. Nici căile nu s-ar potrivi (/api/authentication, /api/scopes, /errors/{code} — nu avem niciun arbore /api/ sau /errors/), dar hostname-ul este motivul pentru care este inaccesibil.
Tratează documentation_url ca pe un identificator stabil pentru tipul de eșec — este de încredere pentru asta, și poți ramifica pe el fără probleme — și mapează-l singur:
documentation_url se termină în | Citește această pagină |
|---|---|
/api/authentication, /api/client-auth, /api/api-keys | Autentificare |
/api/scopes | Autentificare → Scopes |
/api/rate-limits | Rate limits |
/api/errors, /api/errors#validation, /api/errors#server | Această pagină |
/billing/plan-limits | Rate limits → Alte limite |
/webhooks#redelivery | Webhooks → Inspectare și replay |
/errors/{code} | Vocabularul comun de mai sus |