Skip to content

Când ceva eșuează ​

Ce vede efectiv cel care apelează API-ul, cel care primește webhook-uri și inginerul de gardă atunci când ceva merge prost, și ce ai de făcut în fiecare caz.

Tabelul complet de coduri e pe Coduri de eroare. Pagina asta e jumătatea practică: ce eșecuri se reîncearcă, care se repară și unde te uiți când un apel a mers prost și nimeni nu știe de ce.

Body-ul de eroare al API-ului ​

Fiecare răspuns non-2xx de la /api/v1 are forma asta. E snake_case și e împachetat în error:

json
{
  "error": {
    "type": "validation_error",
    "message": "The request payload failed validation.",
    "errors": { "secrets": ["The secrets field is required."] },
    "documentation_url": "https://docs.callagent.dev/api/errors#validation",
    "request_id": "01M1ZKS6SVXR3CHEV87WCSFM60"
  }
}
CâmpCând apare
typeMereu. Categoria — vezi tabelul de mai jos.
messageMereu. Se poate arăta unui dezvoltator; nu conține niciodată textul brut al excepției.
request_idMereu. Un ULID, sau X-Request-Id-ul trimis de tine dacă avea cel mult 64 de caractere. Citează-l într-un raport de bug.
errorsDoar la eșecuri de validare. Pe câmpuri, în forma Laravel.
codeLa eșecuri de furnizor și de plafon de plan.
retryableLa eșecuri de furnizor, de rate limit și de server.
provider, provider_request_idLa eșecuri de furnizor.
documentation_urlDe obicei.

Valorile lui type ​

typeHTTPCe înseamnăCe faci
validation_error422Body-ul tău e greșit. errors spune care câmp.Repară și retrimite.
authentication_error401Cheie lipsă, invalidă sau revocată.Verifică cheia. Fereastra de suprapunere la rotire poate să fi expirat.
permission_error403Cheii îi lipsește scope-ul, sau restricția de origin/IP a refuzat-o.Lărgește scope-ul sau apelează de la un origin permis.
not_found404Nu există resursa — și asta întoarce și o resursă a altui tenant.Verifică id-ul și organizația căreia îi aparține cheia.
rate_limit_error429Prea multe request-uri.Așteaptă Retry-After secunde. Vezi Limite de rate.
plan_limit_exceeded402Un plafon comercial, nu unul tehnic. limit și used sunt în body.Fă upgrade sau șterge ceva.
provider_error502 / 504 / 429Un vendor a respins sau a expirat. provider îl numește.Citește retryable.
ssrf_blocked422Un URL dat de tine se rezolvă undeva unde nu are voie.Folosește un host public, care se rezolvă.
invalid_state422Acțiunea nu se aplică în starea asta.
http_errorvariazăO eroare HTTP de la nivelul framework-ului.
server_error500E a noastră. retryable: true, și e raportată pe server.Reîncearcă o dată, apoi citează request_id.

404 e intenționat identic pentru „nu există" și „există în altă organizație". Nu e o scăpare — dacă le-ai deosebi, i-ai spune unui atacator care id-uri sunt reale.

Două endpoint-uri răspund în afara acestui format

Middleware-ul de chei de API întoarce propriul body de 401/403, fără request_id, pentru că rulează înaintea renderer-ului. Iar POST /webhook-deliveries/{id}/redeliver își construiește propriul 422 pentru „livrarea asta nu poate fi retrimisă". Amândouă folosesc aceleași nume de câmp error.type / error.message, deci un receptor care se uită la ele funcționează în continuare.

Ce se reîncearcă ​

Zece coduri de eroare se pot reîncerca, iar retryable: true e derivat din lista aia, 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

Restul eșuează identic la a doua încercare. Reîncearcă-le pe cele zece; repară-le pe celelalte.

Când reîncerci POST /calls, trimite același idempotency_key. Altfel o reîncercare după un timeout sună omul de două ori. Vezi Telefonie.

Când un apel eșuează ​

POST /calls răspunde 201 de îndată ce există rândul de apel. Asta nu e o promisiune că a fost sunat cineva. Citește status și ended_reason.

Cele 20 de valori ended_reason ​

GrupValori
A închis cinevacustomer_hung_up, operator_hung_up, assistant_ended, assistant_ended_call_tool, transferred_away, api_request
Conversația s-a epuizatsilence_timeout, max_duration_reached, voicemail_detected
Carrier-ul a refuzatno_answer, busy, rejected
Noi am refuzatinsufficient_credit, concurrency_limit, destination_blocked
S-a stricat cevapipeline_error, provider_error, telephony_error, gateway_lost
unknown

ended_by spune cine: customer, assistant, operator, system, api, supervisor.

Cele trei care înseamnă „am refuzat noi" ​

Astea sunt cele pentru care se deschid bug-uri, și fiecare are o cauză precisă:

  • concurrency_limit — included_concurrency + burst_concurrency al organizației era deja ocupat, sau o limită per campanie, per număr sau per trunk era. Verifică apelurile concurente înainte să presupui un bug.
  • destination_blocked — politica de destinație a refuzat. Blocklist, politică de țară, un prefix premium (1900, 1976), de satelit (870–874, 881–883) sau cu cost mare, un plafon orar, zilnic sau de minute pe zi, ori o rută de ieșire marcată explicit ca blocată.
  • insufficient_credit — soldul din ledger, un plafon zilnic de cheltuială, un plafon de volum al planului sau un blocaj de dunning.

telephony_error acoperă un set precis: destinație lipsă, nicio rută, from lipsă, un caller ID neverificat (ai cerut să prezinți un număr care nu e al organizației tale), un furnizor necunoscut, un furnizor neimplementat, o credențială lipsă sau un eșec de conectare la carrier.

gateway_lost înseamnă că voice gateway-ul a murit în timpul apelului și un reaper a finalizat rândul. Transcrierea se oprește acolo unde s-a oprit gateway-ul.

Unde te uiți, în ordine ​

bash
curl "$CALLAGENT_API_URL/calls/$CALL_ID"        -H "Authorization: Bearer $KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/events" -H "Authorization: Bearer $KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/debug"  -H "Authorization: Bearer $KEY"
  1. ended_reason de pe apel. De nouă ori din zece e de ajuns.
  2. events — se populează chiar și pentru un apel care nu s-a conectat niciodată. call.created, apoi call.state.changed pentru fiecare tranziție, cu offset_ms de la începutul apelului. Ultimul eveniment dinaintea eșecului e cel care contează.
  3. metrics, dacă s-a conectat dar a părut lent — desface turul pe etape de pipeline.
  4. debug pentru tot pachetul.

Un apel care se conectează și apoi moare în tăcere ​

Eșecul care merită numit, pentru că e ușor de provocat și imposibil de văzut din API: configurația de apel a gateway-ului a fost respinsă, deci nu are ce rula.

Configurația e aruncată întreagă — nu parțial — dacă lipsește oricare dintre astea: model.provider, model.model, transcriber.provider, voice.provider, voice.voice_id. GET /assistants/{id}/validate verifică patru din cinci și ratează voice_id.

voice_id e și cel pe care nu îl poți seta prin API: POST/PATCH /assistants ciuntesc orice sub-cheie din voice, model, transcriber și server care nu are o regulă de validare, iar voice.voice_id e una dintre ele. Verifică cu GET /assistants/{id} — dacă voice se întoarce ca {"provider": "…"} și nimic altceva, asistentul acela nu poate porni un apel. Setează vocea din dashboard. Detalii complete pe Ce nu este construit.

E respinsă și dacă transcriber-ul e unul pentru care gateway-ul nu are adaptor. Pe aia validate o prinde, și de asta merită apelat înainte de publicare.

Când un webhook eșuează ​

Din partea ta, o livrare care nu ajunge e una dintre:

Endpoint-ul tău a întors 4xxNu se reîncearcă. Un 404 sau 410 e tratat exact ca un 400 — ai spus nu.
Endpoint-ul tău a întors 5xx sau a expiratSe reîncearcă la 10 s, 30 s, 2 min, 10 min, 30 min, apoi din oră în oră, până la max_attempts (implicit 6).
URL-ul tău a fost blocat de garda SSRFMort imediat. O reîncercare către o destinație blocată e blocată din nou.
Te-ai abonat la ceva ce nu se declanșeazăO listă events goală exclude cele patru tipuri de volum mare.
20 de eșecuri consecutiveEndpoint-ul e dezactivat. active: false, cu disabled_reason setat. Un singur succes resetează contorul.

GET /webhook-deliveries?status=dead e primul lucru de verificat. Fiecare livrare păstrează statusul răspunsului, body-ul (redactat) și eroarea, iar pentru fiecare încercare există un rând separat, doar cu inserări, cu IP-ul rezolvat.

Dacă handler-ul tău a rulat dar ai răspuns încet, o să primești o reîncercare a unui eveniment pe care deja l-ai procesat. Deduplică pe X-CallAgent-Event-Id — vezi Webhooks.

Când un tool eșuează ​

Un eșec de tool nu doboară niciodată apelul. Întoarce un rezultat structurat către model, care poate spune ceva util și merge mai departe.

RezultatCauză
not_foundUn tool function fără URL de server, sau un tool MCP care a dispărut.
tool_timeoutBugetul de execuție al tool-ului a expirat. Se poate reîncerca.
internal_errorTool-ul a aruncat. Caută tool.failed în GET /calls/{id}/events.
RespinsPolitică, nivel de risc sau o confirmare refuzată. Asistentul spune linia din rejection_plan.

Un tool pe care modelul nu l-a apelat deloc e de obicei:

  • un tool de tip bash, computer, text_editor, client_side sau custom_provider — astea sunt aruncate din fiecare apel, în tăcere;
  • un tool cu un nume pe care modelul nu îl poate adresa. Orice iese din [A-Za-z0-9_-] e aruncat, nu stâlcit, pentru că un tool redenumit devine neapelabil într-un fel pe care nimic din interfață nu îl arată;
  • un tool api_request fără config.url, aruncat la conversie;
  • un tool nepublicat, sau atașat cu pin_mode: pinned la o versiune care nu mai există.

GET /assistants/{id}/preview listează ce s-a rezolvat efectiv, cu exposed_name.

Cum raportezi o problemă ​

Include:

  • request_id din body-ul de eroare, sau ULID-ul apelului;
  • ended_reason și ended_by;
  • config_hash al asistentului, din GET /assistants/{id}/versions, ca să se poată identifica exact configurația;
  • momentul, cu offset. Totul e stocat ISO 8601 cu offset explicit.

Mai departe ​

Scrisă după cod. Ce nu este încă implementat este marcat ca atare, niciodată descris ca funcțional.