Sviluppatori

Gestisci agenti, numeri e chiamate dai tuoi sistemi e aggiungi una chiamata vocale al tuo sito web.

Panoramica

L'API è un'API REST. I corpi di richiesta e risposta sono in JSON (UTF-8), i timestamp sono in secondi unix e gli importi in USD. L'URL di base è:

https://hubvocal.com/v1

Una chiave API appartiene a una sola organizzazione e agisce con permessi di amministratore al suo interno. Tutto ciò che puoi fare nella dashboard puoi farlo con l'API:

RisorsaEndpoint
Agenti/v1/agents, /v1/agents/{id}
Knowledge base/v1/knowledge, /v1/knowledge/{id}, /v1/knowledge/search
Strumenti/v1/tools, /v1/tools/{id}, /v1/tools/{id}/test
Server MCP/v1/mcp-servers, /v1/mcp-servers/{id}, /v1/mcp-servers/{id}/sync
Webhook/v1/webhooks, /v1/webhooks/{id}, /v1/webhooks/{id}/deliveries
Voci e motori/v1/voices
Chiamate web/v1/web-calls
Chiamate e chiamate in uscita/v1/calls, /v1/calls/{room}
Numeri/v1/numbers, /v1/numbers/{number}
Utilizzo/v1/usage

Un documento OpenAPI 3.1 leggibile dalle macchine è disponibile su GET /v1/openapi.json e non richiede una chiave. Per tutti i dettagli su ogni endpoint, consulta il riferimento API.

Guida rapida

  1. Crea una chiave API

    Nella dashboard apri Impostazioni > Chiavi API e dai alla chiave un nome che ne indichi lo scopo. Solo i proprietari e gli amministratori dell'organizzazione possono creare chiavi. Il segreto (vt_live_…) viene mostrato una sola volta: salvalo subito in un secrets manager.

    shell
    export VT_API_KEY="vt_live_…"
  2. Elenca i tuoi agenti

    Il modo più rapido per verificare che la chiave funzioni. Nei passaggi successivi userai un id preso dalla risposta.

    curl
    curl "https://hubvocal.com/v1/agents" \
      -H "Authorization: Bearer $VT_API_KEY"
  3. Crea un agente

    Motore, lingua, voce e istruzioni in un unico oggetto. Per i nomi delle voci validi e i limiti, chiama GET /v1/voices.

    curl
    curl -X POST "https://hubvocal.com/v1/agents" \
      -H "Authorization: Bearer $VT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Front desk",
      "lang": "en",
      "engine": "live",
      "instructions": "You are the front desk assistant for a dental clinic. Help callers book appointments and answer general questions briefly. Never make things up; offer a callback from the team instead.",
      "greeting": "Hello, thanks for calling the clinic. How can I help you today?",
      "voice": {
        "voice": "marin",
        "speed": 1
      },
      "tools": {
        "builtins": [
          "end_call",
          "take_message"
        ],
        "webhooks": [
          "tl_8f3a2c"
        ]
      },
      "turn": {
        "first_speaker": "agent",
        "idle_check_s": 30
      },
      "recording": {
        "enabled": false,
        "announce": true
      },
      "post_call": {
        "summary": true,
        "fields": [
          {
            "name": "appointment_date",
            "type": "date",
            "description": "Requested appointment date, if any"
          },
          {
            "name": "callback",
            "type": "boolean",
            "description": "Does the caller want a callback?"
          }
        ]
      },
      "knowledge": {
        "doc_ids": [
          "kb_7d21e4"
        ],
        "mode": "tool"
      }
    }'
  4. Avvia una chiamata web

    Ti colleghi dal browser con server_url e token presenti nella risposta; i dettagli sono più sotto.

    curl
    curl -X POST "https://hubvocal.com/v1/web-calls" \
      -H "Authorization: Bearer $VT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "agent_id": "front-desk"
    }'

Autenticazione

Invia la chiave come token Bearer nell'header Authorization di ogni richiesta:

HTTP
Authorization: Bearer vt_live_…
  • La chiave agisce con permessi di amministratore nella tua organizzazione: crea agenti, assegna numeri ed effettua chiamate. Trattala come una password.
  • Non inserire mai la chiave nel codice del browser, in un'app mobile o in un repository. Per le chiamate dal browser, ottieni un token a breve durata sul tuo server.
  • L'elenco in Impostazioni > Chiavi API mostra quando ogni chiave è stata usata l'ultima volta. Una chiave revocata smette di funzionare immediatamente.
  • Usa una chiave per ogni integrazione, così revocarne una non blocca le altre.

Integra una chiamata web

I tuoi visitatori possono parlare con l'agente direttamente sul tuo sito, nel browser. Il flusso è in tre passaggi:

  1. Il visitatore preme un pulsante e la tua pagina chiama il tuo server.
  2. Il tuo server chiama POST /v1/web-calls con la chiave API e restituisce la risposta (server_url, token, room, max_seconds) alla pagina.
  3. La pagina si collega alla stanza con livekit-client e attiva il microfono. L'agente entra da solo e pronuncia il saluto.

1. Server: ottieni un token

Node (Express)
// server.js (Node 18+, Express)
import express from "express";

const app = express();

app.post("/voice/session", async (req, res) => {
  // Applica qui la tua autenticazione e il tuo rate limiting.
  const r = await fetch("https://hubvocal.com/v1/web-calls", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VT_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ agent_id: "front-desk" }),
  });
  res.status(r.status).json(await r.json());
});

app.listen(3000);

2. Browser: collegati

Se usi un bundler, installa con npm install livekit-client@2.23.0 e importa gli stessi nomi. L'esempio qui sotto carica una versione fissa da una CDN.

index.html
<button id="talk" type="button">Inizia a parlare</button>

<script type="module">
  import { Room, RoomEvent, Track } from "https://cdn.jsdelivr.net/npm/livekit-client@2.23.0/dist/livekit-client.esm.mjs";

  const button = document.getElementById("talk");
  let room;

  button.addEventListener("click", async () => {
    if (room) {
      await room.disconnect();
      return;
    }
    button.disabled = true;
    // Il tuo server restituisce così com'è la risposta di POST /v1/web-calls.
    const res = await fetch("/voice/session", { method: "POST" });
    const session = await res.json();
    if (!res.ok) {
      button.disabled = false;
      alert(session.detail);
      return;
    }

    room = new Room();
    // Collega l'audio dell'agente a un elemento <audio> quando arriva.
    room.on(RoomEvent.TrackSubscribed, (track) => {
      if (track.kind === Track.Kind.Audio) document.body.appendChild(track.attach());
    });
    room.on(RoomEvent.Disconnected, () => {
      room?.remoteParticipants.forEach((p) =>
        p.audioTrackPublications.forEach((pub) => pub.track?.detach().forEach((el) => el.remove())),
      );
      room = undefined;
      button.textContent = "Inizia a parlare";
    });

    await room.connect(session.server_url, session.token);
    await room.localParticipant.setMicrophoneEnabled(true);
    button.disabled = false;
    button.textContent = "Riaggancia";
  });
</script>

Avvia la connessione da un gesto dell'utente (un clic): i browser vi legano il permesso del microfono e la riproduzione automatica dell'audio. La pagina deve essere servita in HTTPS.

Una chiamata dura al massimo max_seconds, poi l'agente riaggancia. Quando termina, recupera il registro con GET /v1/calls/{room}.

Chiamate in uscita

Chiama un numero da uno dei numeri della tua organizzazione; l'agente parla appena la chiamata riceve risposta. Il numero from deve essere presente in GET /v1/numbers. Senza agent_id viene usato l'agente assegnato a quel numero. I numeri sono in formato E.164 (+90…).

curl
curl -X POST "https://hubvocal.com/v1/calls" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "from": "+908501234567",
  "to": "+905321234567",
  "agent_id": "front-desk"
}'

Risposta

JSON
{
  "room": "tel-out-9c41e0b7",
  "provider": "didww",
  "sip_call_id": "SCL_7mVt2kQb9xRa"
}

Il campo room nella risposta identifica la chiamata. Quando la chiamata termina, il registro (trascrizione, costo, chi ha riagganciato) è disponibile su GET /v1/calls/{room}. Le chiamate senza risposta terminano con ended_by impostato su no_answer.

Chiamate in entrata

Alle chiamate in entrata sui tuoi numeri risponde l'agente assegnato a ciascun numero. I numeri arrivano tramite DIDWW, Twilio o il tuo SIP trunk (Asterisk e altri) e vengono aggiunti alla tua organizzazione dal team della piattaforma. Sei tu a scegliere quale agente risponde:

curl
curl -X PUT "https://hubvocal.com/v1/numbers/%2B908501234567" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "agent_id": "front-desk",
  "label": "Main line"
}'

Versioni e pubblicazione sicura

Modificando un agente cambi la sua bozza. Le chiamate sui tuoi numeri, le chiamate web e quelle in uscita usano la versione pubblicata, quindi puoi lavorare sulla bozza senza toccare le chiamate reali.

  1. Modifica l'agente con PUT /v1/agents/{id}. Cambia solo la bozza; draft_changed diventa true.
  2. Prova la bozza: avvia una chiamata web con version: "draft".
  3. Pubblica con POST /v1/agents/{id}/publish e una nota facoltativa. La bozza diventa una nuova versione numerata e va online.
  4. Se qualcosa non va, POST /v1/agents/{id}/rollback con un numero di versione precedente la ripubblica come nuova versione.
curl
curl -X POST "https://hubvocal.com/v1/agents/front-desk/publish" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Shorter greeting"
}'

POST /v1/web-calls e POST /v1/calls accettano version: "live" (predefinito), "draft" o un numero di versione. Un agente mai pubblicato usa la sua bozza.

curl
curl -X POST "https://hubvocal.com/v1/web-calls" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "agent_id": "front-desk",
  "version": "draft"
}'

Testa prima di pubblicare

Il laboratorio di test esegue conversazioni testuali tra un chiamante simulato e il tuo agente, così puoi verificare una modifica prima che la sentano i chiamanti reali.

  1. Scrivi uno scenario: chi chiama (persona), cosa vuole (goal) e fino a 20 turni.
  2. Aggiungi le regole che una buona chiamata deve rispettare (expect), ad esempio "chiede il nome del chiamante prima di prenotare".
  3. Fornisci risultati di esempio per gli strumenti (mocks); nessuno strumento o webhook reale viene chiamato.
  4. Esegui gli scenari sulla bozza. Un modello separato verifica ogni regola e spiega perché è stata rispettata o meno.
JSON
{
  "scenarios": [
    {
      "name": "Booking without a name",
      "persona": "A caller in a hurry who gives short answers",
      "goal": "Book an appointment tomorrow afternoon",
      "max_turns": 12,
      "expect": [
        "Asks for the caller's name before booking",
        "Reads the time back and asks for confirmation"
      ],
      "mocks": {
        "check_availability": {
          "ok": true,
          "slots": [
            "2026-11-06T14:00",
            "2026-11-06T15:30"
          ]
        }
      }
    }
  ],
  "version": "draft"
}

Le conversazioni si svolgono in modalità testo, quindi verificano le istruzioni e l'uso degli strumenti, non la voce né la latenza. Le simulazioni si avviano dalla dashboard; per ora non esiste un endpoint /v1 dedicato.

Strumenti

Gli strumenti permettono all'agente di agire durante una chiamata. La stessa definizione funziona su tutti e quattro i motori; su GPT-Live li esegue il modello in background. Gli strumenti si attivano per ogni agente nel campo tools.

Strumenti integrati

NomeCosa fa
end_callL'agente saluta e riaggancia 0,75 s dopo aver finito di parlare. Se nel frattempo il chiamante parla, non riaggancia. Il registro della chiamata riporta ended_by: "agent".
take_messageRaccoglie name, phone, message e callback_requested e li scrive in outcomes[] nel registro della chiamata. Non chiede conferma.
search_knowledgeAggiunto automaticamente quando l'agente ha documenti in knowledge. Cerca al loro interno e salva le fonti nel registro della chiamata.
check_availabilityLettura. Restituisce gli slot liberi tra date_from e date_to, nell'orario di lavoro dell'agente. Aggiunto quando calendar è impostato.
book_appointmentScrittura, con passaggio di conferma. Prenota start a nome di name con phone, più una note facoltativa. La prenotazione finisce in outcomes[].
cancel_appointmentScrittura, con passaggio di conferma. Annulla una prenotazione tramite il suo booking_ref.

Strumenti webhook

Uno strumento webhook è un endpoint HTTPS sul tuo sistema. Definiscilo una sola volta per l'organizzazione nella dashboard, in Strumenti, oppure con POST /v1/tools, poi aggiungi il suo id a tools.webhooks di un agente (fino a 16 per agente).

curl
curl -X POST "https://hubvocal.com/v1/tools" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 8,
  "enabled": true,
  "secret": "whsec_2b7f…"
}'
  • name: lettere minuscole, cifre e trattini bassi, da 3 a 41 caratteri; deve iniziare con una lettera ed essere unico nell'organizzazione. end_call e take_message sono riservati.
  • description dice al modello quando usare lo strumento. Scrivila come un'istruzione per un collega.
  • parameters è un oggetto JSON Schema con un massimo di 20 campi di tipo string, number, integer o boolean (enum consentito).
  • kind è read o write; timeout_s va da 2 a 15 secondi.

La richiesta che inviamo

Quando il modello chiama lo strumento, l'API invia una richiesta POST al tuo URL. Il corpo contiene il nome dello strumento, gli argomenti scelti dal modello e alcune informazioni di contesto sulla chiamata:

HTTP
POST /hooks/order HTTP/1.1
Host: crm.example.com
Content-Type: application/json
X-VT-Timestamp: 1793832421
X-VT-Signature: sha256=5f0c9a…

{
  "tool": "check_order",
  "arguments": {
    "order_id": "A-10234"
  },
  "call": {
    "room": "tel-6b1f04d2",
    "org_id": "6f1c2a40-3b7e-4d0c-9a51-2f8e7c1d9b03",
    "agent": "front-desk",
    "channel": "phone_in",
    "caller": "+905321234567",
    "called": "+908501234567",
    "lang": "en"
  }
}

Ogni richiesta è firmata con il segreto dello strumento. X-VT-Timestamp è l'ora unix; X-VT-Signature è sha256= seguito dall'HMAC-SHA256 in esadecimale di timestamp + "." + body. Verificala sul corpo grezzo prima di interpretarlo e rifiuta i timestamp più vecchi di qualche minuto.

Verificare la firma

Node (Express)
// server.js (Node 18+, Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.VT_TOOL_SECRET;

// La firma copre i byte esatti: leggi il corpo grezzo.
app.post("/hooks/order", express.raw({ type: "*/*" }), (req, res) => {
  const ts = req.get("X-VT-Timestamp") ?? "";
  const sig = req.get("X-VT-Signature") ?? "";

  // Rifiuta le richieste vecchie (protezione dal replay).
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);

  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(ts + ".").update(req.body).digest("hex");
  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  const { tool, arguments: args, call } = JSON.parse(req.body.toString("utf8"));
  // Rispondi in breve: al modello arrivano solo i primi 4 KB.
  res.json({ order_id: args.order_id, status: "shipped", eta: "2026-11-07" });
});

app.listen(3000);

La tua risposta

Rispondi con uno stato 2xx e JSON o testo semplice. Al modello arrivano solo i primi 4 KB, quindi restituisci solo ciò che l'agente deve dire. Uno stato diverso da 2xx o un timeout segnalano al modello che lo strumento non ha funzionato, e l'agente gestisce la situazione nella conversazione.

Strumenti di lettura e scrittura

  • read: strumenti eseguiti subito, per consultazioni come lo stato di un ordine o gli orari di apertura.
  • write: strumenti che prevedono un passaggio di conferma. La prima chiamata allo strumento non viene eseguita; l'agente rilegge i dettagli al chiamante e chiede un sì o un no. Lo strumento viene eseguito solo se nel turno successivo il chiamante dice chiaramente sì nella lingua della chiamata.
  • La stessa chiamata allo strumento (nome e argomenti) viene eseguita al massimo una volta per conversazione.

Sicurezza

  • Gli URL devono usare https://. Gli indirizzi locali e di reti private vengono rifiutati.
  • Il segreto viene salvato cifrato e l'API non lo restituisce mai (has_secret indica se è impostato). L'agente non lo vede mai: la richiesta la effettua l'API.
  • Argomenti e risposte non vengono salvati. Il registro della chiamata conserva tools[] con nome, tipo, esito e durata, oltre a una corsia TOOL nella timeline.

Usa POST /v1/tools/{id}/test per inviare una richiesta reale con argomenti di esempio e vedere stato, tempi e corpo della risposta.

curl
curl -X POST "https://hubvocal.com/v1/tools/tl_8f3a2c/test" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "arguments": {
    "order_id": "A-10234"
  }
}'

Knowledge base

La knowledge base contiene i documenti della tua organizzazione. Quando un agente ha dei documenti selezionati, riceve lo strumento search_knowledge e risponde in base a ciò che trova. Ha l'istruzione di non inventare ciò che non è nei documenti.

Aggiungi un documento

Invia un file come multipart (file), testo semplice come JSON (title, text) o una singola pagina web come JSON (url). L'elaborazione avviene in background: il documento parte come processing e diventa ready o failed.

curl
curl -X POST "https://hubvocal.com/v1/knowledge" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -F "file=@price-list-2026.pdf"
  • File: pdf, docx, txt, md o html, fino a 10 MB ciascuno.
  • Fino a 200 documenti e 200 MB in totale per organizzazione.
  • URL: una sola pagina, solo https; gli indirizzi locali e privati vengono rifiutati.

Assegnali a un agente

Elenca gli id dei documenti in knowledge.doc_ids dell'agente (fino a 50). Prova qualche ricerca con POST /v1/knowledge/search prima di andare in produzione.

curl
curl -X POST "https://hubvocal.com/v1/knowledge/search" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "how much is a scale and polish"
}'

Nel registro della chiamata ogni ricerca compare in tools[] come search_knowledge, con le sue sources (documento, titolo e frammento).

Calendario e appuntamenti

Con un calendario collegato, l'agente può verificare la disponibilità e prenotare o annullare appuntamenti durante la chiamata.

I calendari si collegano dalla dashboard, in Integrazioni (Google Calendar). Non esiste un endpoint /v1 per le integrazioni: nell'API indichi solo quale integrazione usa un agente.

Impostazioni dell'agente

Imposta l'oggetto calendar dell'agente: integrazione e calendario, durata dell'appuntamento (15–240 min), intervallo tra appuntamenti (0–60 min), fuso orario, orario di lavoro, preavviso minimo (0–72 h) e con quanto anticipo massimo si può prenotare (1–90 giorni).

PUT /v1/agents/front-desk
{
  "calendar": {
    "integration_id": "int_4a7c10",
    "calendar_id": "primary",
    "duration_min": 30,
    "buffer_min": 10,
    "timezone": "Europe/Istanbul",
    "working_hours": {
      "mon": [
        [
          "09:00",
          "18:00"
        ]
      ],
      "tue": [
        [
          "09:00",
          "18:00"
        ]
      ],
      "wed": [
        [
          "09:00",
          "18:00"
        ]
      ],
      "thu": [
        [
          "09:00",
          "18:00"
        ]
      ],
      "fri": [
        [
          "09:00",
          "17:00"
        ]
      ]
    },
    "min_notice_h": 2,
    "max_days_ahead": 30
  }
}

L'agente riceve quindi tre strumenti integrati: check_availability (lettura), book_appointment e cancel_appointment (entrambi di scrittura). Quelli di scrittura passano dal passaggio di conferma: l'agente ripete orario, nome e telefono e prenota solo dopo un sì chiaro.

Ogni prenotazione viene salvata nel registro della chiamata in outcomes[] come {kind: "appointment", start, name, phone, ref}.

MCP

HubVocal supporta il Model Context Protocol in due modi: i tuoi agenti possono usare strumenti dei tuoi server MCP e i client MCP possono gestire la piattaforma.

Collega un server MCP ai tuoi agenti

Aggiungi il tuo server con POST /v1/mcp-servers o dalla dashboard. Recuperiamo il suo elenco di strumenti (tools/list); i nuovi strumenti arrivano disattivati. Attiva quelli che ti servono, segna ciascuno come read o write e inseriscili in tools.mcp dell'agente.

curl
curl -X POST "https://hubvocal.com/v1/mcp-servers" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "crm",
  "url": "https://mcp.example.com/mcp",
  "auth": "bearer",
  "secret": "sk_live_…"
}'
  • La connessione la stabilisce l'API, non l'agente, quindi l'agente non vede mai il segreto del tuo server.
  • Gli URL devono essere https; gli indirizzi locali e privati vengono rifiutati, anche al momento della connessione.
  • Gli strumenti write passano dal passaggio di conferma: l'agente ripete i dettagli ed esegue lo strumento solo dopo un sì chiaro, al massimo una volta per chiamata.
  • Ogni invocazione va in timeout dopo 10 secondi; al modello arrivano al massimo 4 KB del risultato.
  • Fino a 10 server per organizzazione e 50 strumenti per server. Gli strumenti MCP rientrano nel limite di 16 strumenti dell'agente.

Nella conversazione e nel registro della chiamata, uno strumento MCP si chiama <server>.<tool>, per esempio crm.find_customer.

Gestisci la piattaforma da un client MCP

HubVocal è anche un server MCP all'indirizzo https://hubvocal.com/mcp (streamable HTTP). Autenticati con una chiave API dell'organizzazione come token Bearer. Il limite di richieste è condiviso con /v1.

Collega un client

Sostituisci vt_live_… con la tua chiave. Usa una chiave con i soli scope di cui il client ha bisogno.

Claude Code
claude mcp add --transport http hubvocal https://hubvocal.com/mcp \
  --header "Authorization: Bearer vt_live_…"

Nell'app ChatGPT i connettori MCP personalizzati si aggiungono in modalità sviluppatore; la possibilità di usare un header con la chiave API dipende dal tuo piano e dalle impostazioni del connettore. Con l'API di OpenAI puoi passare la chiave direttamente, come qui sotto.

Strumenti

StrumentoScopeCosa fa
list_agentsreadElenca i tuoi agenti.
get_agentreadLegge un agente.
list_callsreadElenca le chiamate, filtrate per since, outcome o agent_id (fino a 50).
get_callreadLegge una chiamata con trascrizione e riepilogo.
list_numbersreadElenca i tuoi numeri di telefono.
list_knowledgereadElenca i documenti della knowledge base.
search_knowledgereadCerca nella knowledge base.
get_usagereadCosti e minuti del mese in corso.
create_agentwriteCrea un agente.
update_agentwriteModifica la bozza di un agente.
publish_agentwritePubblica la bozza di un agente.
add_knowledge_textwriteAggiunge un documento di testo alla knowledge base.
start_outbound_callcallsAvvia una chiamata in uscita (agent_id, to). Valgono le consuete regole e i limiti delle chiamate in uscita.

Risorse e prompt

  • agent://{id}: le impostazioni di un agente.
  • call://{id}: la trascrizione di una chiamata in formato testo.
  • Prompt daily_call_report: un riepilogo delle chiamate della giornata.

Scope e log di audit

Ogni chiave API ha degli scope, che scegli quando la crei nella dashboard:

  • read: strumenti ed endpoint di sola lettura.
  • write: in più, creare e modificare agenti e documenti.
  • calls: in più, avviare chiamate in uscita.

Le chiavi create prima dell'introduzione degli scope li hanno tutti e tre. Ogni chiamata a uno strumento MCP viene registrata nel log di audit con chiave, strumento, ora e risultato.

Widget web

Il widget aggiunge un pulsante di chiamata al tuo sito web. I visitatori parlano con il tuo agente nel browser; non serve codice lato server da parte tua.

Crea una chiave pubblica

Nella dashboard, apri l'agente e crea una chiave pubblica (pk_…). Una chiave pubblica appartiene a un solo agente e può stare nella tua pagina senza rischi, perché consente solo chiamate web con quell'agente e solo dai domini che autorizzi.

  • Domini consentiti (allowed_origins): le chiamate da altri siti vengono rifiutate.
  • Un limite di durata per ogni chiamata.
  • Al massimo 10 chiamate all'ora dallo stesso indirizzo IP.

Aggiungi lo script

Incollalo prima del tag di chiusura </body>:

HTML
<script src="https://hubvocal.com/widget.js" data-agent="pk_…" async></script>

Colore, posizione e testo del pulsante (nella lingua del visitatore) provengono dalle impostazioni widget dell'agente: color, position, label.

JSON
{
  "widget": {
    "color": "#2563eb",
    "position": "bottom-right",
    "label": "Talk to us"
  }
}

Il widget chiama POST /api/public/web-calls con la chiave pubblica; non vede mai la tua chiave API. Le chiavi pubbliche si gestiscono nella dashboard.

Campi strutturati verso il tuo CRM

I campi strutturati trasformano una conversazione in dati utilizzabili dai tuoi sistemi, ad esempio la data dell'appuntamento, il prodotto richiesto o se la persona vuole essere richiamata.

Definisci i campi

Aggiungi fino a 24 campi in post_call.fields dell'agente. Ognuno ha un name (lettere minuscole, cifre e trattini bassi), un type (string, number, boolean, enum o date), una description che indica al modello cosa cercare e, facoltativamente, valori enum e required.

JSON
{
  "post_call": {
    "summary": true,
    "fields": [
      {
        "name": "appointment_date",
        "type": "date",
        "description": "Requested appointment date"
      },
      {
        "name": "service",
        "type": "enum",
        "enum": [
          "checkup",
          "cleaning",
          "whitening"
        ],
        "description": "Service the caller asked about"
      },
      {
        "name": "callback",
        "type": "boolean",
        "required": true,
        "description": "Does the caller want a callback?"
      }
    ]
  }
}

Dopo la chiamata, lo stesso modello che scrive il riepilogo compila i campi. I valori non menzionati sono null. Li ricevi come extracted in GET /v1/calls/{room} e nell'elenco delle chiamate, oltre che nel corpo del webhook call.ended.

JSON
{
  "extracted": {
    "appointment_date": "2026-11-12",
    "service": "cleaning",
    "callback": true
  }
}

L'esportazione CSV include ogni campo come colonna.

Webhook delle chiamate

Un webhook invia un evento call.ended al tuo sistema quando una chiamata è terminata e il suo registro, riepilogo compreso, è completo. Usalo per aprire un ticket, aggiornare un contatto nel CRM o inviare un follow-up.

Se i punteggi di qualità delle chiamate sono attivi, puoi anche iscriverti a call.quality_low. Viene inviato una volta per chiamata quando il punteggio scende sotto la soglia della tua organizzazione.

Aggiungi fino a 5 webhook per organizzazione dalla dashboard o con POST /v1/webhooks. Se non invii un secret, ne viene generato uno, restituito solo in quella risposta. Gli URL devono essere https; gli indirizzi locali e privati vengono rifiutati.

curl
curl -X POST "https://hubvocal.com/v1/webhooks" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://crm.example.com/hooks/voice",
  "events": [
    "call.ended"
  ]
}'

L'evento

data è lo stesso oggetto chiamata restituito da GET /v1/calls/{room}, con la trascrizione e il riepilogo:

HTTP
POST /hooks/voice HTTP/1.1
Host: crm.example.com
Content-Type: application/json
X-VT-Event: call.ended
X-VT-Delivery: dl_51b7a9
X-VT-Timestamp: 1793832440
X-VT-Signature: sha256=9b1e44…

{
  "id": "evt_8a41f0c2",
  "type": "call.ended",
  "created_at": "2026-11-05T09:47:20Z",
  "data": {
    "room": "demo-8585522d",
    "org_id": "6f1c2a40-3b7e-4d0c-9a51-2f8e7c1d9b03",
    "org_name": "Example Clinic",
    "channel": "web",
    "profile": "Front desk",
    "engine": "live",
    "lang": "en",
    "voice": "marin",
    "started_at": 1793832409.1,
    "ended_at": 1793832434.2,
    "duration_s": 25.1,
    "total_usd": 0.021215,
    "ended_by": "caller",
    "outcome": "message_taken",
    "summary_text": "The caller asked for an appointment on Thursday afternoon; a message was taken for a callback.",
    "extracted": {
      "appointment_date": "2026-11-12",
      "callback": true
    },
    "first_audio_s": 2.43,
    "turns": 4,
    "source_gaps": {
      "count": 2,
      "total_ms": 230,
      "max_ms": 141,
      "agent_audio_s": 31.2,
      "events": [
        [
          16.4,
          141
        ],
        [
          40.1,
          89
        ]
      ]
    },
    "quality": {
      "verdict": "Clean: no loss, no gaps.",
      "protocol": "UDP",
      "loss_pct": 0,
      "conceal_pct": 0,
      "jitter_ms": 4,
      "rtt_ms": 38
    },
    "cost": {
      "live": {
        "billed_s": 25.1,
        "reported": true,
        "voice_usd": 0.020917,
        "backend": {
          "in": 1840,
          "cached": 1024,
          "out": 96
        },
        "backend_usd": 0.000298
      },
      "llm": {
        "input_tokens": 1840,
        "cached_tokens": 1024,
        "output_tokens": 96,
        "usd": 0.000298
      },
      "tts": {
        "input_tokens": 0,
        "audio_tokens": 0,
        "audio_s": 25.1,
        "usd": 0.020917
      },
      "stt": {
        "audio_s": 0,
        "usd": 0
      }
    },
    "tools": [
      {
        "t": 14.2,
        "name": "check_order",
        "kind": "read",
        "ok": true,
        "ms": 412,
        "status": 200
      }
    ],
    "outcomes": [],
    "summary": {
      "text": "The caller asked for an appointment on Thursday afternoon; a message was taken for a callback.",
      "language": "en",
      "outcome": "message_taken",
      "sentiment": "neutral",
      "topics": [
        "appointment"
      ],
      "action_items": [
        "Call the caller back on Thursday morning"
      ]
    },
    "recording": {
      "available": true,
      "duration_s": 25.1,
      "size_bytes": 201344
    },
    "supervisors": [
      {
        "user": "ayse@example.com",
        "joined_at": 1793832420.4,
        "left_at": 1793832431,
        "modes": [
          "listen",
          "whisper"
        ]
      }
    ],
    "greeting": {
      "connect_s": 0.15,
      "session_started_s": 0.05,
      "reply_requested_s": 0.05,
      "first_audio_s": 2.4,
      "reply_done_s": 7.1
    },
    "transcript": [
      {
        "role": "assistant",
        "text": "Hello, thanks for calling the clinic. How can I help you today?",
        "t": 2.4,
        "interrupted": false
      },
      {
        "role": "user",
        "text": "I would like an appointment on Thursday afternoon.",
        "t": 9.8
      }
    ]
  }
}

Le richieste sono firmate esattamente come le richieste degli strumenti: X-VT-Timestamp e X-VT-Signature. Si aggiungono altri due header: X-VT-Event (il tipo di evento) e X-VT-Delivery (un id univoco per questa consegna).

Nuovi tentativi

Qualsiasi risposta 2xx entro 10 secondi vale come consegnata. Altrimenti riproviamo:

  1. dopo 1 minuto
  2. dopo 5 minuti
  3. dopo 30 minuti
  4. dopo 2 ore

Dopo 5 tentativi in totale, la consegna viene segnata come failed. Puoi vedere ogni tentativo con GET /v1/webhooks/{id}/deliveries e rimettere in coda una consegna non riuscita con POST /v1/webhooks/deliveries/{id}/retry.

Gestisci i duplicati

Un nuovo tentativo può arrivare anche se la tua risposta precedente si è persa per strada, quindi lo stesso evento può arrivare più di una volta. Salva l'id X-VT-Delivery e ignora quelli già elaborati. Rispondi subito e sposta il lavoro lento in background.

Node (Express)
// Verifica prima la firma (stesso codice degli strumenti).
app.post("/hooks/voice", express.raw({ type: "*/*" }), async (req, res) => {
  if (!verify(req)) return res.sendStatus(401);

  // Consegna già elaborata? Conferma la ricezione e fermati.
  const delivery = req.get("X-VT-Delivery");
  if (await db.deliveries.exists(delivery)) return res.sendStatus(200);
  await db.deliveries.insert(delivery);

  // Rispondi subito; il lavoro lento fallo in background.
  res.sendStatus(200);
  const event = JSON.parse(req.body.toString("utf8"));
  queue.add("call-ended", event.data);
});

POST /v1/webhooks/{id}/test invia un evento call.ended di esempio e restituisce lo stato, i tempi e il corpo della risposta.

Lingue di conversazione supportate

Le lingue in cui ogni motore può sostenere una conversazione. La lingua di un agente si imposta con il campo lang, usando i codici qui sotto.

  • Supportata
  • Sperimentale Il motore non elenca ufficialmente questa lingua. Funziona se indicato nelle istruzioni, ma non ne abbiamo misurato la qualità.
Lingua lang GPT-Live 1Gemini LiveOpenAI RealtimeClassico
Türkçetr tr Sperimentale Supportata Sperimentale Supportata
Englishen en Supportata Supportata Sperimentale Supportata
Deutschde de Supportata Supportata Sperimentale Supportata
Françaisfr fr Supportata Supportata Sperimentale Supportata
Españoles es Sperimentale Supportata Sperimentale Supportata
Italianoit it Sperimentale Supportata Sperimentale Supportata
Portuguêspt pt Supportata Supportata Sperimentale Supportata
Nederlandsnl nl Sperimentale Supportata Sperimentale Supportata
Русскийru ru Sperimentale Supportata Sperimentale Supportata
Українськаuk uk Sperimentale Supportata Sperimentale Supportata
Polskipl pl Sperimentale Supportata Sperimentale Supportata
Românăro ro Sperimentale Supportata Sperimentale Supportata
Ελληνικάel el Sperimentale Supportata Sperimentale Supportata
Българскиbg bg Sperimentale Supportata Sperimentale Supportata
Češtinacs cs Sperimentale Supportata Sperimentale Supportata
Slovenčinask sk Sperimentale Supportata Sperimentale Supportata
Magyarhu hu Sperimentale Supportata Sperimentale Supportata
Hrvatskihr hr Sperimentale Supportata Sperimentale Supportata
Српскиsr sr Sperimentale Supportata Sperimentale Supportata
Bosanskibs bs Sperimentale Supportata Sperimentale Supportata
Slovenščinasl sl Sperimentale Supportata Sperimentale Supportata
Македонскиmk mk Sperimentale Supportata Sperimentale Supportata
Svenskasv sv Sperimentale Supportata Sperimentale Supportata
Danskda da Sperimentale Supportata Sperimentale Supportata
Norskno no Sperimentale Supportata Sperimentale Supportata
Suomifi fi Sperimentale Supportata Sperimentale Supportata
Eestiet et Sperimentale Supportata Sperimentale Supportata
Latviešulv lv Sperimentale Supportata Sperimentale Supportata
Lietuviųlt lt Sperimentale Supportata Sperimentale Supportata
Catalàca ca Sperimentale Supportata Sperimentale Supportata

Prova le lingue sperimentali nel browser prima di metterle su una linea telefonica.

Limiti ed errori

Il limite è di 120 richieste al minuto per chiave. Oltre questa soglia ricevi 429 con un header Retry-After in secondi. Gli errori arrivano con un codice di stato HTTP e sempre con lo stesso corpo; detail è un messaggio leggibile.

402
{
  "detail": "Monthly spending limit reached"
}
CodiceSignificatoQuando
400Richiesta non validaNon è stato possibile leggere il corpo, manca un campo obbligatorio o un valore non è valido (ad esempio un numero non in E.164 o l'eliminazione dell'ultimo agente).
401Non autorizzatoManca l'header Authorization, la chiave non inizia con vt_live_ oppure è stata revocata.
402Limite raggiuntoL'organizzazione ha raggiunto il limite di spesa mensile; non è possibile avviare nuove chiamate.
403SospesaL'organizzazione è sospesa; non è possibile avviare nuove chiamate.
404Non trovatoAgente, chiamata, numero o endpoint inesistente.
409ConflittoPOST /v1/agents è stato inviato con un id già esistente.
429Troppe richiestePiù di 120 richieste al minuto con la stessa chiave. Attendi i secondi indicati in Retry-After.
502Errore del fornitoreIl fornitore di telefonia ha rifiutato la chiamata in uscita o non ha risposto.
500Errore del serverSi è verificato un errore imprevisto. Riprova; se persiste, scrivici.

Ripeti solo le richieste che ricevono 429 e 5xx. Un 4xx indica un problema nella richiesta stessa: inviarla di nuovo non cambierà il risultato.

Nuovi tentativi con Retry-After
async function vt(path, init = {}, attempt = 0) {
  const res = await fetch(`https://hubvocal.com/v1${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.VT_API_KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  if (res.status === 429 && attempt < 3) {
    const wait = Number(res.headers.get("Retry-After") ?? 60);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return vt(path, init, attempt + 1);
  }
  const data = await res.json();
  if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
  return data;
}