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.
Authorization: Bearer ca_sk_9zK…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ă:
| Tip | Cheia în clar | key_prefix stocat | Unde îi este locul |
|---|---|---|---|
private | ca_sk_ + 48 de caractere | ca_sk | Serverul tău. Niciodată într-un browser, într-un binar mobil sau într-un repository git. |
public | ca_pk_ + 48 de caractere | ca_pk | Un bundle de browser. Poate fi publicată în siguranță. |
service_account | ca_sk_ + 48 de caractere | ca_sk | Machine-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 ──▶ sessionJWT-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ție | Câmp | Aplicată la un request REST | La eșec |
|---|---|---|---|
| Scope-uri | scopes | Per endpoint, în handler — vezi mai jos | 403 |
| Origin-uri | allowed_origins | Da, la fiecare request | 403 |
| Allowlist de IP-uri | allowed_ips | Da, la fiecare request | 403 |
| Assistants | allowed_assistant_ids | Nu — 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șicall.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:
| Grup | Scope-uri |
|---|---|
| Assistants | assistant.read assistant.write assistant.delete assistant.publish squad.read squad.write |
| Tools și knowledge | tool.read tool.write tool.delete mcp.read mcp.write knowledge.read knowledge.write |
| Apeluri | call.read call.create call.end call.delete call.listen recording.read recording.delete transcript.read |
| Telefonie | phone_number.read phone_number.write trunk.read trunk.write extension.read extension.write ivr.write |
| Contact centre | queue.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 |
| Engage | campaign.read campaign.write chat.read chat.write |
| Observe | analytics.read board.write eval.read eval.write scorecard.write monitor.write issue.write |
| Developeri | api_key.read api_key.write webhook.read webhook.write |
| Platformă | credential.read credential.write integration.write storage.write registry.read |
| Clienți | customer.read customer.write customer.delete |
| Organizație | organization.read organization.write member.read member.invite member.write role.write audit.read sso.write scim.write environment.write |
| Facturare | billing.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.
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âmp | Tip | Obligatoriu | Note |
|---|---|---|---|
public_key | string | da | maxim 128 de caractere |
assistant_id | ULID | — | Verificat față de allowed_assistant_ids al cheii |
user_id | string | — | maxim 128 de caractere; devine claim-ul user |
session_id | string | — | maxim 64 de caractere; se generează un ULID când este omis |
ttl | integer | — | secunde, 60–3600, implicit 900 |
{
"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.
{
"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_hours | Cheia veche |
|---|---|
0 | Expirată și revocată imediat |
1–720 | Expiră după suprapunere; până atunci este încă utilizabilă |
| omis | 24 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ție | Status | error.type | error.message |
|---|---|---|---|
| Nicio cheie pe request | 401 | authentication_error | Missing API key. |
| Cheie necunoscută, revocată sau expirată | 401 | authentication_error | Invalid or revoked API key. |
| Origin nepermis | 403 | authentication_error | This API key is not permitted from this origin. |
| IP nepermis | 403 | authentication_error | This API key is not permitted from this address. |
| Organizație inactivă | 403 | organization_error | This organization is not active. |
| Scope lipsă | 403 | permission_error | This 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.