Skip to content

Autentificare ​

Fiecare request către /api/v1 poartă o cheie API. Cheia identifică organizația, așa că nu există niciun parametru organization_id nicăieri în acest API și nimic de falsificat.

Cele două header-e ​

Oricare funcționează. Authorization este cel de folosit; X-CallAgent-Key există pentru clienții care nu pot seta un header Authorization.

http
Authorization: Bearer ca_sk_9zK…
http
X-CallAgent-Key: ca_sk_9zK…

Bearer este comparat case-sensitive, cu exact un spațiu după el. Bearer key (două spații) sau bearer key este citit ca o cheie lipsă, nu ca una greșită.

Tipuri de chei și prefixe ​

Serverul aplică un prefix la emitere. Există exact două:

TipCheia în clarkey_prefix stocatUnde îi este locul
privateca_sk_ + 48 de caractereca_skServerul tău. Niciodată într-un browser, într-un binar mobil sau într-un repository git.
publicca_pk_ + 48 de caractereca_pkUn bundle de browser. Poate fi publicată în siguranță.
service_accountca_sk_ + 48 de caractereca_skMachine-to-machine. Împarte prefixul privat — tipul este o coloană, nu ceva ce poți vedea în cheie.

O cheie are 54 de caractere: prefixul de șase caractere, apoi 48 de caractere din [A-Za-z0-9]. key_prefix-ul stocat nu are underscore la final, motiv pentru care o cheie redactată se afișează ca ca_sk…mF3q, nu ca ca_sk_…mF3q.

Nu pk_live_ / sk_test_

Cheia nu conține mediul. ca_sk_ este același string în development și în producție, iar nu există o variantă _live_/_test_ a niciunuia dintre prefixe. Dacă ai văzut pk_live_ într-o notă internă de design, acea schemă a fost propusă și niciodată implementată — ca_sk_ și ca_pk_ sunt ce scrie serverul. Orice string pk_live_ sau sk_test_ pe care îl găsești în acest codebase aparține Stripe, nu CallAgent.

O cheie privată este stocată doar ca sha256(key), așa că o scurgere a bazei de date nu oferă nimic ce poate fi reluat — iar o cheie pierdută se rotește, nu se recuperează niciodată. O cheie publică este stocată în plus și în clar, pentru că SDK-ul trebuie să ți-o poată arăta din nou.

Ce poate și ce nu poate face o cheie publică ​

O cheie publică poate fi livrată în siguranță într-un bundle de browser. Nu este o cheie privată mai slabă; este prima jumătate a unui schimb:

browser  ──  ca_pk_…  ──▶  POST /client-tokens  ──▶  short-lived JWT  ──▶  session

JWT-ul este limitat la o singură organizație, opțional la un singur assistant și o singură sesiune, și expiră. Vezi token-uri de client mai jos.

O cheie publică se autentifică momentan la fel ca una privată

Control plane-ul stochează un key_hash și pentru cheile publice, nu doar pentru cele private, așa că o cheie ca_pk_… prezentată ca Authorization: Bearer este acceptată pe endpoint-urile REST obișnuite, supusă doar restricțiilor sale de origin și IP. Tratează asta ca pe un detaliu de implementare care poate fi înăsprit, nu ca pe un mod suportat de a apela API-ul: restricționează fiecare cheie publică la origin-urile care au voie să o folosească și nu-i acorda niciodată scope-uri de care nu are nevoie pentru schimbul de token.

Restricții pe o cheie ​

Unei chei i se pot atașa patru restricții. Doar trei dintre ele sunt aplicate la un request API obișnuit.

RestricțieCâmpAplicată la un request RESTLa eșec
Scope-uriscopesPer endpoint, în handler — vezi mai jos403
Origin-uriallowed_originsDa, la fiecare request403
Allowlist de IP-uriallowed_ipsDa, la fiecare request403
Assistantsallowed_assistant_idsNu — doar la emiterea unui client token—

Origin-urile sunt glob-uri fnmatch (https://*.example.com). O cheie cu o listă de origin-uri nevidă respinge un request care nu poartă deloc un header Origin, adică orice request din curl — deci nu pune o restricție de origin pe o cheie pe care o apelezi de pe un server.

IP-urile sunt comparate ca string-uri exacte cu adresa clientului. Aplicația are încredere în X-Forwarded-For primit de la proxy-ul său.

Restricția de assistant nu este o restricție generală a cheii. allowed_assistant_ids limitează pentru ce assistant poate fi emis un client token. Nu împiedică cheia în sine să citească sau să scrie alt assistant prin REST. Dacă ai nevoie de această graniță, folosește scope-uri și chei separate.

Scopes ​

Un scope este <resource>.<verb>. O cheie poate purta:

  • o listă goală — toate scope-urile. Acesta este comportamentul implicit și este o acordare reală, nu o valoare nesetată.
  • * — toate scope-urile, explicit.
  • <resource>.* — toate verbele pe acea resursă. call.* acordă call.read, call.create, call.end, call.delete și call.listen.
  • scope-uri exacte — call.read, webhook.write, …

GET /api-keys/scopes returnează întregul vocabular, grupat, cu o descriere pentru fiecare. Acel endpoint nu impune niciun scope propriu, deci orice cheie validă îl poate citi.

Cele 86 de scope-uri, pe grupuri:

GrupScope-uri
Assistantsassistant.read assistant.write assistant.delete assistant.publish squad.read squad.write
Tools și knowledgetool.read tool.write tool.delete mcp.read mcp.write knowledge.read knowledge.write
Apeluricall.read call.create call.end call.delete call.listen recording.read recording.delete transcript.read
Telefoniephone_number.read phone_number.write trunk.read trunk.write extension.read extension.write ivr.write
Contact centrequeue.read queue.write operator.read operator.write operator.monitor operator.whisper operator.barge operator.force_state wallboard.read team.read team.write team.manage_any
Engagecampaign.read campaign.write chat.read chat.write
Observeanalytics.read board.write eval.read eval.write scorecard.write monitor.write issue.write
Developeriapi_key.read api_key.write webhook.read webhook.write
Platformăcredential.read credential.write integration.write storage.write registry.read
Cliențicustomer.read customer.write customer.delete
Organizațieorganization.read organization.write member.read member.invite member.write role.write audit.read sso.write scim.write environment.write
Facturarebilling.read billing.write

Aplicarea scope-urilor se face per endpoint și nu este universală

Scope-urile sunt verificate în interiorul controllerului care are nevoie de ele, nu de un middleware care acoperă tot. Fiecare pagină de endpoint din referința API precizează scope-ul pe care acel endpoint îl impune efectiv și spune clar când nu impune niciunul.

Două consecințe de care merită să ții cont când planifici. Prima: o acordare <resource>.* este validată doar ca formă: nonsense.* este acceptat și stocat, doar că nu se potrivește niciodată cu nimic. A doua: un scope pe care nu îl acorzi nu este o graniță pe care te poți baza pentru fiecare endpoint — acolo unde o pagină spune că nu se impune niciun scope, orice cheie validă ajunge la el. Dă-i unei chei setul cel mai restrâns care funcționează azi, dar tratează separarea cheilor, nu scope-urile, drept graniță de securitate între două sisteme.

Token-uri de client ​

POST /client-tokens schimbă o cheie publică pe un JWT de scurtă durată.

Ruta se află în grupul autentificat, așa că request-ul are nevoie de o cheie bearer și repetă cheia publică în body. În practică, browserul trimite același string ca_pk_… de două ori.

bash
curl -X POST "$CALLAGENT_API_URL/client-tokens" \
  -H "Authorization: Bearer ca_pk_YOUR_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "public_key": "ca_pk_YOUR_PUBLIC_KEY",
       "assistant_id": "01JQ0000000000000000000000",
       "ttl": 900
     }'
CâmpTipObligatoriuNote
public_keystringdamaxim 128 de caractere
assistant_idULID—Verificat față de allowed_assistant_ids al cheii
user_idstring—maxim 128 de caractere; devine claim-ul user
session_idstring—maxim 64 de caractere; se generează un ULID când este omis
ttlinteger—secunde, 60–3600, implicit 900
json
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "token_type": "Bearer",
    "expires_at": "2026-01-01T12:15:00+00:00",
    "expires_in": 900,
    "organization_id": "01JQ…",
    "assistant_id": "01JQ…",
    "session_id": "01JQ…"
  }
}

JWT-ul este HS256. Claim-urile sale sunt iss (callagent), aud (callagent-client), jti, iat, nbf, exp, organization, assistant, user, session, origins, key și scopes. Valorile null sunt omise, nu trimise ca null.

POST /client-tokens/verify primește {"token": "…"} și răspunde 200 cu {"data": {"valid": true, "claims": {…}}}, sau 401 cu un code care numește motivul:

malformed · bad_algorithm · bad_signature · not_yet_valid · expired · wrong_audience · wrong_origin · key_revoked

Revocarea este reverificată la fiecare verify față de claim-ul key, așa că revocarea unei chei API invalidează imediat orice token încă valabil emis din ea.

Emiterea și rotirea cheilor ​

POST /api-keys returnează cheia în clar exact o singură dată, într-un câmp key care nu apare nicăieri altundeva — nici în GET /api-keys, nici în resursă, nici în baza de date.

json
{
  "data": {
    "id": "01JQ…",
    "name": "Billing sync",
    "type": "private",
    "key": "ca_sk_…",
    "key_prefix": "ca_sk",
    "last_four": "mF3q",
    "scopes": ["billing.read"],
    "expires_at": null,
    "notice": "…"
  }
}

O cheie publică trebuie restricționată la cel puțin un origin; crearea uneia fără allowed_origins este un 422, pentru că o cheie publică fără restricție de origin este vizibilă pentru oricine îți deschide site-ul și utilizabilă de toți.

POST /api-keys/{apiKey}/rotate emite un înlocuitor care poartă același nume, aceleași scope-uri și aceleași restricții, și pune cheia veche pe un cronometru:

overlap_hoursCheia veche
0Expirată și revocată imediat
1–720Expiră după suprapunere; până atunci este încă utilizabilă
omis24 de ore

Deployează cheia nouă în timpul suprapunerii, confirmă că traficul s-a mutat și las-o pe cea veche să expire. DELETE /api-keys/{apiKey} revocă imediat, fără suprapunere.

Cum arată o autentificare eșuată ​

CondițieStatuserror.typeerror.message
Nicio cheie pe request401authentication_errorMissing API key.
Cheie necunoscută, revocată sau expirată401authentication_errorInvalid or revoked API key.
Origin nepermis403authentication_errorThis API key is not permitted from this origin.
IP nepermis403authentication_errorThis API key is not permitted from this address.
Organizație inactivă403organization_errorThis organization is not active.
Scope lipsă403permission_errorThis API key does not carry the [assistant.write] scope.

Revocată, expirată și niciodată-existentă sunt deliberat imposibil de deosebit. La fel este și resursa altei organizații, care returnează 404 în loc de 403 — un 403 ar confirma că rândul există.

Erorile din middleware nu poartă request_id

Primele patru rânduri — și verificarea organizației — sunt produse de middleware care rulează înaintea renderer-ului central de erori, deci poartă type și message, dar nu și request_id, iar verificarea organizației nu poartă nici documentation_url. Cel de-al doilea 403, cel pentru scope lipsă, este ridicat în interiorul unui handler și poartă un request_id. Vezi erori.

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