Telefonie
De unde sosesc apelurile și pe unde pleacă: numere cumpărate printr-un cont de carrier gestionat, numere pe care le ai deja, conturile din spatele lor și rutele care decid prin ce trunk pleacă un apel.
Pentru trunk-ul tău de carrier, vezi Trunking SIP.
Ce poate face fiecare carrier
Sunt declarate nouă id-uri de furnizor. Șase au adaptor; zadarma, didlogic și chime aruncă un 501 în care se numesc și îți spun să aduci DID-ul peste un trunk SIP.
Fiecare adaptor își declară propria matrice de capabilități, și diferă enorm. Citește matricea, nu numele. GET /telephony/providers întoarce versiunea vie.
| Twilio | Telnyx | Plivo | DIDWW | BYO SIP | Custom SIP | |
|---|---|---|---|---|---|---|
| Caută numere disponibile | ✅ | ✅ | ✅ | ✖ | ✖ | ✖ |
| Cumpără / eliberează | ✅ | ✅ | ✅ | ✖ | ✖ | ✖ |
| Listează numerele din cont | ✅ | ✅ | ✅ | ✅ | ✖ | ◐ |
| Configurează webhook-urile numărului | ✅ | ✅ | ✅ | ✖ | — | — |
| Voce de intrare | ✅ | ✅ | ✖ | ✅¹ | ✅ | ✅ |
| Voce de ieșire | ✅ | ✅ | ✅ | ✖ | ✅ | ✅ |
| Media streams (ajungerea în pipeline-ul AI) | ✅ | ⚠️² | ✖ | — | ✅³ | ✅³ |
| Înregistrare | ✅ | ✅ | ✖ | — | ✅ | ✅ |
| Înregistrare pe două canale | ✅ | ✅ | ✖ | — | ✅ | ✅ |
| Transfer | ✅ | ✅ | ✖ | ✖ | ✅ | ✅ |
| DTMF | ✅ | ✅ | ✅ | ✖ | ✅ | ✅ |
| Detecție de robot telefonic | ✅ | ◐⁴ | ✖ | ✖ | ✖ | ✖ |
| SMS / MMS | ✅ | ✅ | ✅ (SMS) | ✖ | ✖ | ✖ |
| Semnătură de webhook | ✅ | ✅ | ✅ | ✖ | ✅ | ✅ |
| Trunking SIP | ◐⁵ | ✅ | — | — | ✅ | ✅ |
✅ implementat · ◐ parțial · ✖ neimplementat · — nu se aplică
- Purtată peste un trunk SIP CallAgent, nu peste API-ul propriu al DIDWW.
- Telnyx declară
media_streamsca implementat și nimic din cod nu construiește o instrucțiune de media stream pentru Telnyx.placeOutboundCall()trimite un URL de webhook Call-Control și se oprește. Un apel Telnyx nu ajunge azi la un asistent. E o capabilitate declarată care contrazice runtime-ul — tratează runtime-ul ca autoritar. - Declarat implementat, și blocat în practică de aceeași predare media care blochează SIP-ul. Vezi SIP și Ce nu este construit.
- Telnyx expune AMD doar pe leg-uri Call Control pornite cu
answering_machine_detectionsetat la apelare, niciodată retroactiv. - Twilio Elastic SIP Trunking se configurează în afara acestui API.
Rezumatul practic
Twilio e singurul carrier dovedit că duce un apel în pipeline-ul AI. Restul ori declară o capabilitate pe care nu o pot livra cap-coadă, ori nu o declară.
Comentariul propriu al DIDWW explică de ce calea de cumpărare lipsește, în loc să fie un stub: o comandă DIDWW e un flux în mai mulți pași (țară → oraș → grup de DID-uri → comandă → pool de capacitate) care are nevoie de decizii de produs pe care nu le-a luat nimeni, iar interfața nu are voie să ofere un buton Cumpără care merge pe jumătate.
Conturi de telefonie și credențiale
Un cont leagă o credențială de furnizor de o organizație. Credențialele sunt criptate cu o cheie separată de APP_KEY (CredentialCipher, decizia D-004), deci rotirea cheii aplicației nu atinge secretele de furnizor ale tenanților.
GET/POST /api/v1/telephony/accounts
GET/PATCH/DELETE /api/v1/telephony/accounts/{id}
POST /api/v1/telephony/accounts/{id}/health
GET /api/v1/telephony/providersCredențiala Twilio e account_sid + auth_token; cele Telnyx și DIDWW sunt api_key. Vezi Pornire rapidă, pasul 2.
Numere
API-ul ăsta e camelCase
Endpoint-urile de telefonie primesc nume de câmp în camelCase (friendlyName, destinationType, sipTrunkId). Endpoint-urile de asistenți, tools și apeluri primesc snake_case. E o inconsecvență a produsului; paginile de aici arată ce acceptă efectiv fiecare endpoint.
Înregistrează un număr pe care îl ai deja
curl -X POST "$CALLAGENT_API_URL/telephony/phone-numbers" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"number": "+40312345901",
"friendlyName": "Suport",
"assistantId": "01JQ…",
"capabilities": ["voice"]
}'Valorile din capabilities sunt voice, sms, mms, fax. Rândul se creează cu provider: "byo_sip", iar dacă dai assistantId (sau squadId) se setează pentru tine destination_type și destination_id.
POST /telephony/phone-numbers/import e versiunea în masă: listează fiecare număr din contul tău de furnizor și creează rândurile.
Cumpără unul
curl -X POST "$CALLAGENT_API_URL/telephony/phone-numbers/search" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "provider": "twilio", "country": "RO", "type": "local", "limit": 20 }'provider și un country din două litere sunt obligatorii. type e local, mobile, toll_free sau national și e implicit local. Apoi:
curl -X POST "$CALLAGENT_API_URL/telephony/phone-numbers/purchase" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "provider": "twilio", "number": "+40312345678", "friendlyName": "Suport" }'O cumpărare are trei pași: cumperi la carrier, inserezi rândul, trimiți webhook-urile. Dacă pasul cu webhook-urile eșuează, numărul nu se dă înapoi — e al tău, deci rândul rămâne, cu webhooks_in_sync: false și cu eșecul adăugat în notes. Rulează configure din nou.
Configurare și eliberare
POST /api/v1/telephony/phone-numbers/{id}/configure trimite URL-urile de webhook la carrier
POST /api/v1/telephony/phone-numbers/{id}/release eliberează la carrier
DELETE /api/v1/telephony/phone-numbers/{id} șterge doar rândul (soft delete)configure nu primește niciun body. Răspunde { "configured": true, "webhooks_in_sync": true } sau un 502. Nu alege o destinație — aia e treaba lui PATCH.
Îndreaptă un număr undeva
curl -X PATCH "$CALLAGENT_API_URL/telephony/phone-numbers/$NUMBER_ID" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destinationType": "assistant",
"destinationId": "01JQ…",
"recordingMode": "dual_channel",
"maxConcurrentCalls": 10
}'Tipurile de destinație, toate zece:
| Tip | Are nevoie de |
|---|---|
assistant, squad, queue, ivr, ring_group, extension, voicemail | destinationId |
external, sip_uri | destinationValue |
hook | hookUrl și câmpurile de hook de mai jos |
Alte câmpuri de PATCH care merită știute: inboundEnabled, outboundEnabled, outboundCallerIdNumber / Name, smsEnabled, smsAssistantId, recordingMode (disabled · mono · dual_channel · inherit), recordingConsentRequired, callerIdPolicyId, status (pending · active · suspended · released · error), metadata.
hookSecret nu e atribuit niciodată pe calea obișnuită — coloana ține text criptat, iar scrierea unui secret brut în atribut l-ar stoca în clar.
Cum e rutat un apel de intrare
Prima potrivire câștigă:
- o rută de intrare care se potrivește pe număr, după prioritate;
- hook-ul dinamic al numărului, dacă
hookEnabled; - destinația statică a numărului;
- doar pentru un apel pe trunk SIP, destinația de rezervă a trunk-ului.
Căutarea numărului trece intenționat peste tenanți, pentru că încă nu e legat niciun tenant, iar organizația e legată apoi din rândul care s-a potrivit — niciodată din ceva aflat în request. Un număr care nu se potrivește cu nimic e respins, nu rutat către o destinație implicită.
Hook-uri dinamice
În loc de o destinație fixă, un număr poate întreba serverul tău în momentul apelului:
| Câmp | Ce înseamnă |
|---|---|
hookUrl | Unde întreabă. |
hookSecret | Semnează request-ul, ca la un webhook. |
hookTimeoutMs | 200–3000. Peste atât, se aplică rezerva. |
hookFallback | static · reject · voicemail — ce se face când serverul tău e lent sau greșit. |
Alege rezerva deliberat. static e iertător; reject e cinstit; nimic nu e mai rău decât un apelant care aude tăcere pentru că un hook a expirat.
Apeluri de ieșire
Două endpoint-uri inițiază un apel, și nu sunt același lucru:
| Endpoint | Când îl folosești |
|---|---|
POST /api/v1/calls | Cel normal. Condus de un asistent, cu verificări de admitere, idempotent, produce o înregistrare completă de apel. snake_case. |
POST /api/v1/telephony/calls | Inițiere printr-un cont de telefonie, direct, pentru apelare în stil PBX care nu e un apel cu asistent. |
curl -X POST "$CALLAGENT_API_URL/calls" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "01JQ…",
"phone_number_id": "01JQ…",
"to": "+40712345678",
"idempotency_key": "comanda-4471-callback",
"answering_machine_detection": true,
"ring_timeout_seconds": 30
}'Body-ul complet: to (obligatoriu), from, assistant_id, assistant_version_id, assistant_overrides, squad_id, phone_number_id, sip_trunk_id, customer_id, campaign_id, transport (pstn · sip · web · mobile), provider, variables, metadata, idempotency_key, answering_machine_detection, ring_timeout_seconds (5–300).
Admiterea
Înainte să existe un leg, în ordinea asta:
- Politica de destinație — organizația activă și neblocată, direcția activată, allowlist, blocklist, politica de țară, clasa numărului (premium
1900/1976, satelit870–874și881–883, Coreea de Nord850, plus restul tabelelor de prefixe cu cost mare), plafoane orare, zilnice și de minute pe zi. - Rutare — trebuie să se potrivească o rută de ieșire; o rută
blockedaruncăroute_blocked. - Caller ID — un pool dintr-o politică de caller ID, sau numărul cerut de tine doar dacă organizația ta îl deține; altfel
unverified_caller_id(403). - Concurență —
included_concurrency + burst_concurrency, plus dimensiunile per campanie, per număr și per trunk. Un refuz econcurrency_limit(429). - Inițiere — iar dacă carrier-ul aruncă, slotul de concurență se eliberează imediat, pentru că nimeni nu va trimite vreodată un hangup pentru un apel care nu a început.
Separat, admiterea de facturare rulează cu verificările ieftine întâi: statusul organizației, expirarea trialului și verificarea telefonului, blocajul de dunning, plafonul zilnic de cheltuială, plafoanele de volum ale planului, fondurile, concurența. Nu debitează un cost estimat în avans; consumul e contorizat continuu.
Refuzurile ajung ca ended_reason pe apel, nu ca un 500 opac:
ended_reason | Cauzat de |
|---|---|
concurrency_limit | Refuz de concurență. |
destination_blocked | Rută blocată, blocklist, țară blocată, prefix blocat, premium, satelit, cost mare, orice plafon. |
telephony_error | Destinație lipsă, nicio rută, from lipsă, caller ID neverificat, furnizor necunoscut, neimplementat, credențială lipsă, eroare de conectare. |
provider_error | Orice altceva a spus carrier-ul. |
Idempotență
Dacă omiți o cheie, ți se generează una. Cu o cheie, o repetare întoarce rândul de apel existent și nu mai sună o dată — impus de un index unic pe (organization_id, idempotency_key), nu doar de logica aplicației.
O singură rezervă: repetarea nu e comparată cu request-ul original. Refolosirea unei chei cu un body diferit întoarce apelul original, nu o eroare.
Rute
GET/POST /api/v1/telephony/inbound-routes
PATCH/DELETE /api/v1/telephony/inbound-routes/{id}
GET/POST /api/v1/telephony/outbound-routes
PATCH/DELETE /api/v1/telephony/outbound-routes/{id}Rutele de intrare se potrivesc pe un număr și bat destinația proprie a numărului. Rutele de ieșire aleg trunk-ul sau contul prin care pleacă un număr apelat, iar o rută poate fi blocked ca să refuze o destinație direct.
Transferuri
Există opt moduri de transfer: blind, warm, assistant_warm, dynamic, sip, phone, queue, on_hold_specialist. Consultative: warm, assistant_warm și on_hold_specialist.
Contractul le scrie altfel
TransferPlanSchema din @callagent/contracts folosește blind, warm, warm_with_summary, warm_with_assistant, hold_specialist. Enum-ul PHP care conduce serviciul de transfer folosește cele opt de mai sus. Nu se mapează unu la unu. Peste API-ul REST folosește ortografia din PHP.
Transferul warm e implementat cu comenzi FreeSWITCH (hold, originate, park, bridge) și nu are echivalent Twilio. Pe un apel Twilio, un transfer „warm" e o intrare în conferință, iar o consultare privată între doi operatori nu e oferită, în loc să fie simulată. Vezi Contact center.
API
GET/POST /api/v1/telephony/phone-numbers
POST /api/v1/telephony/phone-numbers/search
POST /api/v1/telephony/phone-numbers/purchase
POST /api/v1/telephony/phone-numbers/import
GET/PATCH/DELETE /api/v1/telephony/phone-numbers/{id}
POST /api/v1/telephony/phone-numbers/{id}/configure
POST /api/v1/telephony/phone-numbers/{id}/release
GET/POST /api/v1/telephony/accounts
GET/PATCH/DELETE /api/v1/telephony/accounts/{id}
POST /api/v1/telephony/accounts/{id}/health
GET /api/v1/telephony/providers
GET/POST /api/v1/telephony/inbound-routes
PATCH/DELETE /api/v1/telephony/inbound-routes/{id}
GET/POST /api/v1/telephony/outbound-routes
PATCH/DELETE /api/v1/telephony/outbound-routes/{id}
POST /api/v1/telephony/callsParametri și răspunsuri complete: numere, conturi, rute, apeluri de ieșire.