Skip to content

Pornire rapidă ​

De la zero la un asistent publicat, cu un număr îndreptat spre el, folosind API-ul REST.

Fiecare comandă de pe pagina asta a fost rulată pe un control plane local în timp ce pagina se scria. Unde un pas are nevoie de un cont pe care nu îl avem — un carrier, un vendor de model — pagina spune asta în loc să se prefacă.

Ai nevoie de stack-ul pornit. Fă întâi Instalare, apoi verifică:

bash
php apps/control-plane/artisan callagent:check

Afișează câte un rând per dependență. Nu porni până când toate rândurile nu spun OK.

bash
export CALLAGENT_API_URL="http://localhost:8000/api/v1"
export CALLAGENT_API_KEY="ca_sk_…"      # creată la pasul 1

1. Ia o cheie de API ​

Există două feluri, iar diferența contează.

FelPrefixUnde e sigurăCe poate face
Privatăca_sk_Serverul tău, CI-ul tău, terminalul tăuTot ce îi permit scope-urile
Publicăca_pk_Un bundle de browser, o pagină web publicăSe schimbă pe un client token de scurtă durată, nimic altceva

Cheile se creează din dashboard. Nu există comandă artisan și nici comandă de CLI care să genereze una — endpoint-ul REST POST /api-keys are nevoie de o cheie ca să fie apelat, deci prima cheie trebuie să vină dintr-o sesiune autentificată. Deschide control plane-ul, alege organizația și du-te la API keys:

http://localhost:8000/{slug-organizatie}/api-keys

Textul unei chei private se arată o singură dată. După aceea baza de date ține doar sha256(cheie), așa că la „arată-mi-o din nou" răspunsul e „rotește-o".

O cheie publică e refuzată dacă nu îi dai cel puțin un origin permis. Refuzul e intenționat: o cheie publică fără restricție de origin e pur și simplu o cheie scursă.

Verifică dacă cheia merge:

bash
curl "$CALLAGENT_API_URL/api-keys/scopes" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY"

Îți întoarce tot vocabularul de scope-uri, pe grupuri. O cheie creată fără scope-uri le are pe toate; scope-urile îngustează o cheie, nu îi dau nimic în plus.

Merge oricare dintre headere

Sunt acceptate și Authorization: Bearer <cheie>, și X-CallAgent-Key: <cheie>. Folosește-l pe cel care îți vine mai ușor din clientul HTTP.

2. Salvează credențialele de furnizor ​

Un asistent în modul pipeline implicit are nevoie de trei vendori: un transcriber, un model și o voce. Fiecare are nevoie de o credențială salvată pentru organizație.

secrets e obligatoriu. Un apel de creare fără el e un 422, nu un draft.

bash
curl -X POST "$CALLAGENT_API_URL/credentials" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "class": "model",
    "provider": "openai",
    "name": "OpenAI",
    "secrets": { "api_key": "sk-…" }
  }'

class e una dintre telephony, model, transcriber, voice, storage, observability, integration, mcp, oauth, sip.

Numele câmpurilor din secrets diferă de la vendor la vendor, pentru că sunt numele pe care le folosește vendorul:

CredențialăCheile din secrets
OpenAI, Deepgram, ElevenLabs, AssemblyAI, Cartesia, Telnyx, DIDWW, Sonixapi_key
Twilioaccount_sid, auth_token
PlayHTapi_key, user_id
Langfusepublic_key, secret_key, plus un host în config

Acum dovedește că funcționează. Endpoint-ul ăsta face un request HTTP real către vendor, cu cheia pe care ai salvat-o:

bash
curl -X POST "$CALLAGENT_API_URL/credentials/$CREDENTIAL_ID/test" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY"

Răspunde 200 indiferent de verdict, pentru că request-ul a reușit — verdictul credențialei e în payload:

json
{
  "data": {
    "ok": false,
    "verified": true,
    "provider": "OpenAI",
    "message": "OpenAI rejected the credential as unauthenticated. …",
    "http_status": 401,
    "endpoint": "https://api.openai.com/v1/models"
  }
}

Citește ok și verified împreună:

verifiedokCe înseamnă
truetrueVendorul a acceptat cheia. Ăsta e singurul verde.
truefalseVendorul a respins cheia. message spune de ce.
falsefalseNimeni nu l-a întrebat pe vendor. Ori CallAgent nu are un apel de verificare pentru furnizorul ăla, ori lipsește un câmp obligatoriu. Nu e un eșec — e lipsă de dovadă.

Repetă pentru "class": "transcriber" și "class": "voice". Ce furnizori există e în registry: GET /registry/models, GET /registry/transcribers, GET /registry/voices.

3. Creează un asistent ​

API-ul de asistenți e snake_case. (API-ul de telefonie e camelCase. E o inconsecvență a produsului, nu o greșeală de tipar aici — vezi convențiile API.)

bash
curl -X POST "$CALLAGENT_API_URL/assistants" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support Line",
    "language": "ro",
    "first_message_mode": "assistant_speaks_first",
    "first_message": "Bună, ai sunat la suport. Cu ce te pot ajuta?",
    "system_prompt": "Ești agent de suport pentru Example SRL. Fii scurt. Pune o singură întrebare pe rând. Nu inventa niciodată un număr de comandă.",
    "transcriber": { "provider": "deepgram" },
    "model":       { "provider": "openai", "model": "gpt-4o-mini" },
    "voice":       { "provider": "elevenlabs", "voice_id": "rachel" },
    "silence_timeout_seconds": 30,
    "max_duration_seconds": 600
  }'
ts
const auth = { Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}` };
const base = process.env.CALLAGENT_API_URL;

const created = await fetch(`${base}/assistants`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Support Line',
    language: 'ro',
    first_message_mode: 'assistant_speaks_first',
    first_message: 'Bună, ai sunat la suport. Cu ce te pot ajuta?',
    system_prompt: 'Ești agent de suport pentru Example SRL. Fii scurt.',
    transcriber: { provider: 'deepgram' },
    model: { provider: 'openai', model: 'gpt-4o-mini' },
    voice: { provider: 'elevenlabs', voice_id: 'rachel' },
    silence_timeout_seconds: 30,
    max_duration_seconds: 600,
  }),
});

if (!created.ok) throw new Error(await created.text());
const assistantId = (await created.json()).data.id;
python
import os, requests

base = os.environ["CALLAGENT_API_URL"]
auth = {"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"}

response = requests.post(f"{base}/assistants", headers=auth, timeout=30, json={
    "name": "Support Line",
    "language": "ro",
    "first_message_mode": "assistant_speaks_first",
    "first_message": "Bună, ai sunat la suport. Cu ce te pot ajuta?",
    "system_prompt": "Ești agent de suport pentru Example SRL. Fii scurt.",
    "transcriber": {"provider": "deepgram"},
    "model": {"provider": "openai", "model": "gpt-4o-mini"},
    "voice": {"provider": "elevenlabs", "voice_id": "rachel"},
})
response.raise_for_status()
assistant_id = response.json()["data"]["id"]
php
use Illuminate\Support\Facades\Http;

$assistant = Http::withToken(env('CALLAGENT_API_KEY'))
    ->acceptJson()
    ->post(env('CALLAGENT_API_URL').'/assistants', [
        'name' => 'Support Line',
        'language' => 'ro',
        'first_message_mode' => 'assistant_speaks_first',
        'first_message' => 'Bună, ai sunat la suport. Cu ce te pot ajuta?',
        'system_prompt' => 'Ești agent de suport pentru Example SRL. Fii scurt.',
        'transcriber' => ['provider' => 'deepgram'],
        'model' => ['provider' => 'openai', 'model' => 'gpt-4o-mini'],
        'voice' => ['provider' => 'elevenlabs', 'voice_id' => 'rachel'],
    ])
    ->throw()
    ->json('data');

first_message_mode se scrie cu underscore peste API

Cele trei valori acceptate sunt assistant_speaks_first, assistant_speaks_first_with_model_generated_message și assistant_waits_for_user.

Contractul gateway-ului scrie aceleași trei valori cu cratimă (assistant-speaks-first), iar control plane-ul convertește la ieșire. Dacă citești @callagent/contracts și scrii REST, convertește. O valoare cu cratimă peste API e un 422 cu „The selected who speaks first is invalid".

4. Validează, apoi publică ​

Când editezi un asistent modifici draft-ul lui. Un apel rulează o versiune publicată și fixează versiunea cu care a pornit, așa că o republicare în timpul apelului nu poate ajunge la o sesiune vie.

bash
curl "$CALLAGENT_API_URL/assistants/$ASSISTANT_ID/validate" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY"
json
{ "data": { "valid": true, "errors": [] } }

Trebuie să știi ce verifică validate și ce nu. Verifică:

  • că există un nume și un system prompt;
  • că model.provider și model.model sunt amândouă setate;
  • că un asistent pipeline are voice.provider și transcriber.provider;
  • că transcriber-ul e unul pentru care gateway-ul are adaptor și că nu e un vendor batch. Verificarea asta există pentru că un asistent publicat rula cândva cu un transcriber batch, gateway-ul a respins toată configurația apelului, iar apelantul a auzit o linie moartă;
  • că max_duration_seconds e sub plafonul platformei;
  • că modul knowledge-only are cel puțin un knowledge base;
  • că template-urile din prompt și din primul mesaj se parsează.

Nu verifică dacă există o credențială pentru furnizorii pe care i-ai numit, dacă funcționează, sau dacă tools-urile atașate se rezolvă. valid: true fără nicio credențială e răspunsul normal. Dovedește credențialele la pasul 2, nu aici.

API-ul REST nu poate seta voice.voice_id, iar un apel are nevoie de el

E cel mai important lucru de pe pagină, deci citește-l de două ori.

Convertorul care construiește configurația de apel a gateway-ului cere cinci câmpuri: model.provider, model.model, transcriber.provider, voice.provider și voice.voice_id. Dacă lipsește oricare, nu întoarce nimic — nu o configurație parțială — iar apelul nu are ce configurație să ruleze. Apelantul aude o linie moartă.

validate verifică patru din cinci. Nu verifică voice_id. Dar problema reală e mai gravă decât o verificare lipsă:

POST și PATCH /assistants aruncă în tăcere voice.voice_id. Controllerul folosește $request->validated() din Laravel, care întoarce doar cheile care au avut o regulă de validare. Clasa de request declară reguli pentru transcriber.provider, model.provider, model.model, voice.provider și server.url — și pentru nimic altceva din interiorul acelor patru blocuri. Orice altă sub-cheie e aruncată înainte să ajungă în baza de date.

Reprodus pe un control plane viu:

bash
# trimis
"voice": { "provider": "elevenlabs", "voice_id": "rachel", "speed": 1.1 }
# salvat, și întors de următorul GET
"voice": { "provider": "elevenlabs" }

La fel pățesc model.temperature, model.max_tokens, transcriber.model, transcriber.language și orice alt câmp de reglaj de pe cele trei blocuri. 201, niciun avertisment, iar valoarea ciuntită e chiar acolo în răspuns, dacă te uiți.

Consecință: un asistent al cărui bloc de voce a fost scris ultima dată prin API-ul REST nu poate porni un apel. CLI-ul trimite către același endpoint, deci callagent apply are aceeași problemă.

Ce funcționează: blocurile de plan (start_speaking_plan, artifact_plan și restul) nu au sub-reguli, deci trec întregi — la fel ca metadata, variable_values și background_sound. Iar dashboard-ul scrie direct în model, nu prin clasa asta de request, deci un asistent configurat acolo primește un bloc de voce complet.

Până se repară, setează vocea din dashboard. Folosește API-ul pentru tot restul.

preview întoarce snapshot-ul rezolvat, cu toate planurile și valorile implicite completate:

bash
curl "$CALLAGENT_API_URL/assistants/$ASSISTANT_ID/preview" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY"

preview nu e formatul de pe fir al gateway-ului

preview întoarce snapshot-ul rezolvat în forma snake_case a API-ului. Gateway-ul primește acel snapshot trecut printr-o a doua conversie, în forma camelCase a contractului (AssistantSnapshotWire), care în plus aruncă tools-urile pe care nu le poate mapa. Folosește preview ca să vezi ce s-a rezolvat, nu ca să prezici octeții de pe fir.

Publică:

bash
curl -X POST "$CALLAGENT_API_URL/assistants/$ASSISTANT_ID/publish" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY"

201 cu un obiect assistant_version: version, config_hash, tool_refs, published_at. Rândul acela e imutabil.

5. Dă-i un număr ​

Dacă numărul e deja al tău (BYO / SIP) ​

POST /telephony/phone-numbers înregistrează un număr pe care îl controlezi. Endpoint-ul ăsta e camelCase și tot aici îndrepți numărul spre un asistent:

bash
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": "'"$ASSISTANT_ID"'"
  }'

Rândul se întoarce cu provider: "byo_sip", destination_type: "assistant" și destination_id setat pe asistentul tău.

Ca să reîndrepți un număr existent, fă PATCH:

bash
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": "'"$ASSISTANT_ID"'" }'

destinationType e una dintre assistant, squad, queue, ivr, ring_group, extension, voicemail, external, sip_uri, hook.

Dacă vrei să cumperi unul ​

Căutarea are nevoie de provider — nu există carrier implicit:

bash
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" }'

apoi

bash
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" }'

Și căutarea, și cumpărarea apelează carrier-ul, deci au nevoie de o credențială de telefonie funcțională de la pasul 2. Din cele nouă id-uri de furnizor declarate în enum, șase au adaptor și doar două pot căuta și cumpăra: vezi Telefonie.

configure nu înseamnă „îndreaptă-l spre un asistent"

POST /telephony/phone-numbers/{id}/configure nu primește niciun body. Trimite URL-urile de webhook ale CallAgent către carrier pentru numărul acela și raportează { "configured": …, "webhooks_in_sync": … }. Rulează-l după o cumpărare sau după ce se schimbă URL-ul tău public. Rutarea se face cu destinationType / destinationId, mai sus.

6. Inițiază un apel de ieșire ​

bash
curl -X POST "$CALLAGENT_API_URL/calls" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "assistant_id": "'"$ASSISTANT_ID"'",
    "phone_number_id": "'"$NUMBER_ID"'",
    "to": "+40712345678",
    "idempotency_key": "quickstart-001"
  }'

idempotency_key poate călători și ca header Idempotency-Key. El e cel care face ca reîncercarea unui request expirat să fie sigură, în loc să sune omul de două ori: o repetare a aceleiași chei în aceeași organizație întoarce rândul de apel original, neatins.

Un 201 nu înseamnă că apelul s-a conectat

Răspunsul e 201 de îndată ce există rândul de apel. Citește status și ended_reason. Fără un carrier configurat primești exact asta, adică ce s-a întâmplat când s-a scris pagina:

json
{
  "data": {
    "id": "01M1ZKWXMFRDX6TK3QYR2EV9VY",
    "status": "failed",
    "ended_reason": "provider_error",
    "duration_seconds": 0
  }
}

Fiecare apel e admis înainte să existe un leg: statusul organizației, blocajul de dunning, plafonul zilnic de cheltuială, plafoanele de volum ale planului, soldul de credit, spațiul de concurență, politica de destinație (allowlist, blocklist, țară, prefixe premium și de satelit, plafoane orare și zilnice) și proprietatea asupra caller ID-ului. Slotul de concurență se eliberează la finalizare chiar dacă apelul crapă. Un refuz se întoarce ca ended_reason — concurrency_limit, destination_blocked sau telephony_error — nu ca un 500 vag.

7. Citește ce s-a întâmplat ​

bash
curl "$CALLAGENT_API_URL/calls/$CALL_ID"             -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/events"      -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/transcript"  -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/messages"    -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/metrics"     -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/costs"       -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/artifacts"   -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/recording"   -H "Authorization: Bearer $CALLAGENT_API_KEY"
curl "$CALLAGENT_API_URL/calls/$CALL_ID/debug"       -H "Authorization: Bearer $CALLAGENT_API_KEY"

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. Acolo te uiți prima dată când un apel a eșuat și nu știi unde.

metrics desface un tur pe etape de pipeline. costs desface banii pe componente și îți spune care componente au avut măcar un preț. recording e un 404 cu {"error":{"code":"not_found"}} când nu există înregistrare, nu un 200 gol.

În loc de polling, abonează-te — vezi Webhooks — sau redirecționează evenimentele către laptop:

bash
callagent listen --forward-to http://localhost:3000/hooks

Ce nu poți face încă pe un laptop ​

Ca să fim cinstiți cu privire la locul unde se oprește pornirea rapidă:

Stare
Tot ce e mai sus, până la POST /calls inclusivFuncționează local, fără cont extern.
Un apel PSTN realAre nevoie de un cont Twilio, un număr cu voce și un URL public. Twilio e singurul carrier dovedit că ajunge în pipeline-ul AI.
Un apel real pe trunk SIPRegistration-ul și autentificarea pe IP sunt dovedite pe un peer de laborator. Niciun apel pe trunk SIP nu a ajuns vreodată la un asistent — predarea media între FreeSWITCH și gateway nu e construită. Vezi SIP.
Speech-to-speechSe configurează, se salvează și e ignorat la runtime. Vezi Ce nu este construit.
Orice spune efectiv modelul sau voceaAre nevoie de chei de vendor cu credit. Nimic de pe pagina asta nu sintetizează audio.

Sau fă tot asta în git ​

yaml
# assistants/support-line.yaml
name: Support Line
firstMessageMode: assistant-speaks-first
firstMessage: Bună, ai sunat la suport. Cu ce te pot ajuta?
systemPrompt: |
  Ești agent de suport pentru Example SRL.
  Fii scurt. Pune o singură întrebare pe rând. Nu inventa un număr de comandă.
transcriber: { provider: deepgram, model: nova-3 }
model:       { provider: openai, model: gpt-4o-mini, temperature: 0.4 }
voice:       { provider: elevenlabs, voiceId: rachel }
bash
callagent validate
callagent plan       # un diff pe câmpuri, cu valoarea de dinainte și de după
callagent apply

CLI-ul vorbește vocabularul din @callagent/contracts — camelCase și assistant-speaks-first cu cratimă — și convertește singur în ortografia API-ului. Vezi Config as code.

Mai departe ​

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