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:
| Risorsa | Endpoint |
|---|---|
| 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
-
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.export VT_API_KEY="vt_live_…" -
Elenca i tuoi agenti
Il modo più rapido per verificare che la chiave funzioni. Nei passaggi successivi userai un id preso dalla risposta.
curlcurl "https://hubvocal.com/v1/agents" \ -H "Authorization: Bearer $VT_API_KEY"JavaScriptconst res = await fetch("https://hubvocal.com/v1/agents", { headers: { Authorization: `Bearer ${process.env.VT_API_KEY}`, }, }); const data = await res.json(); if (!res.ok) throw new Error(`${res.status}: ${data.detail}`); console.log(data);Pythonimport os import requests resp = requests.get( "https://hubvocal.com/v1/agents", headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"}, timeout=30, ) if not resp.ok: raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}") print(resp.json()) -
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.curlcurl -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" } }'JavaScriptconst res = await fetch("https://hubvocal.com/v1/agents", { method: "POST", headers: { Authorization: `Bearer ${process.env.VT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "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" } }), }); const data = await res.json(); if (!res.ok) throw new Error(`${res.status}: ${data.detail}`); console.log(data);Pythonimport os import requests resp = requests.post( "https://hubvocal.com/v1/agents", headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"}, json={ "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", }, }, timeout=30, ) if not resp.ok: raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}") print(resp.json()) -
Avvia una chiamata web
Ti colleghi dal browser con
server_urletokenpresenti nella risposta; i dettagli sono più sotto.curlcurl -X POST "https://hubvocal.com/v1/web-calls" \ -H "Authorization: Bearer $VT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "front-desk" }'JavaScriptconst res = 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" }), }); const data = await res.json(); if (!res.ok) throw new Error(`${res.status}: ${data.detail}`); console.log(data);Pythonimport os import requests resp = requests.post( "https://hubvocal.com/v1/web-calls", headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"}, json={ "agent_id": "front-desk", }, timeout=30, ) if not resp.ok: raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}") print(resp.json())
Autenticazione
Invia la chiave come token Bearer nell'header Authorization di ogni richiesta:
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:
- Il visitatore preme un pulsante e la tua pagina chiama il tuo server.
- Il tuo server chiama
POST /v1/web-callscon la chiave API e restituisce la risposta (server_url,token,room,max_seconds) alla pagina. - La pagina si collega alla stanza con
livekit-cliente attiva il microfono. L'agente entra da solo e pronuncia il saluto.
1. Server: ottieni un token
// 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); # app.py (Flask)
import os
import requests
from flask import Flask, jsonify
app = Flask(__name__)
@app.post("/voice/session")
def voice_session():
# Applica qui la tua autenticazione e il tuo rate limiting.
r = requests.post(
"https://hubvocal.com/v1/web-calls",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={"agent_id": "front-desk"},
timeout=15,
)
return jsonify(r.json()), r.status_code 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.
<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 -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"
}' const res = await fetch("https://hubvocal.com/v1/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"from": "+908501234567",
"to": "+905321234567",
"agent_id": "front-desk"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.post(
"https://hubvocal.com/v1/calls",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"from": "+908501234567",
"to": "+905321234567",
"agent_id": "front-desk",
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) Risposta
{
"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 -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"
}' const res = await fetch("https://hubvocal.com/v1/numbers/%2B908501234567", {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"agent_id": "front-desk",
"label": "Main line"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.put(
"https://hubvocal.com/v1/numbers/%2B908501234567",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"agent_id": "front-desk",
"label": "Main line",
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) 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.
- Modifica l'agente con
PUT /v1/agents/{id}. Cambia solo la bozza;draft_changeddiventa true. - Prova la bozza: avvia una chiamata web con
version: "draft". - Pubblica con
POST /v1/agents/{id}/publishe una nota facoltativa. La bozza diventa una nuova versione numerata e va online. - Se qualcosa non va,
POST /v1/agents/{id}/rollbackcon un numero di versione precedente la ripubblica come nuova versione.
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"
}' const res = await fetch("https://hubvocal.com/v1/agents/front-desk/publish", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"note": "Shorter greeting"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.post(
"https://hubvocal.com/v1/agents/front-desk/publish",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"note": "Shorter greeting",
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) 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 -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.
- Scrivi uno scenario: chi chiama (
persona), cosa vuole (goal) e fino a 20 turni. - Aggiungi le regole che una buona chiamata deve rispettare (
expect), ad esempio "chiede il nome del chiamante prima di prenotare". - Fornisci risultati di esempio per gli strumenti (
mocks); nessuno strumento o webhook reale viene chiamato. - Esegui gli scenari sulla bozza. Un modello separato verifica ogni regola e spiega perché è stata rispettata o meno.
{
"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
| Nome | Cosa fa |
|---|---|
end_call | L'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_message | Raccoglie name, phone, message e callback_requested e li scrive in outcomes[] nel registro della chiamata. Non chiede conferma. |
search_knowledge | Aggiunto automaticamente quando l'agente ha documenti in knowledge. Cerca al loro interno e salva le fonti nel registro della chiamata. |
check_availability | Lettura. Restituisce gli slot liberi tra date_from e date_to, nell'orario di lavoro dell'agente. Aggiunto quando calendar è impostato. |
book_appointment | Scrittura, con passaggio di conferma. Prenota start a nome di name con phone, più una note facoltativa. La prenotazione finisce in outcomes[]. |
cancel_appointment | Scrittura, 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 -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…"
}' const res = await fetch("https://hubvocal.com/v1/tools", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"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…"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.post(
"https://hubvocal.com/v1/tools",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"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…",
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) name: lettere minuscole, cifre e trattini bassi, da 3 a 41 caratteri; deve iniziare con una lettera ed essere unico nell'organizzazione.end_calletake_messagesono riservati.descriptiondice 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èreadowrite;timeout_sva 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:
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
// 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); # app.py (Flask)
import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, jsonify, request
app = Flask(__name__)
SECRET = os.environ["VT_TOOL_SECRET"].encode()
@app.post("/hooks/order")
def order():
ts = request.headers.get("X-VT-Timestamp", "")
sig = request.headers.get("X-VT-Signature", "")
# Rifiuta le richieste vecchie (protezione dal replay).
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(401)
# La firma copre i byte esatti: leggi il corpo grezzo.
body = request.get_data()
expected = "sha256=" + hmac.new(SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
payload = json.loads(body)
args = payload["arguments"]
# Rispondi in breve: al modello arrivano solo i primi 4 KB.
return jsonify(order_id=args["order_id"], status="shipped", eta="2026-11-07") 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_secretindica 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 -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"
}
}' const res = await fetch("https://hubvocal.com/v1/tools/tl_8f3a2c/test", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"arguments": {
"order_id": "A-10234"
}
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.post(
"https://hubvocal.com/v1/tools/tl_8f3a2c/test",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"arguments": {
"order_id": "A-10234",
},
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) 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 -X POST "https://hubvocal.com/v1/knowledge" \
-H "Authorization: Bearer $VT_API_KEY" \
-F "file=@price-list-2026.pdf" import fs from "node:fs";
const form = new FormData();
form.append("file", new Blob([await fs.promises.readFile("price-list-2026.pdf")]), "price-list-2026.pdf");
const res = await fetch("https://hubvocal.com/v1/knowledge", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.VT_API_KEY}` },
body: form,
});
console.log(await res.json()); import os
import requests
with open("price-list-2026.pdf", "rb") as f:
resp = requests.post(
"https://hubvocal.com/v1/knowledge",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
files={"file": ("price-list-2026.pdf", f, "application/pdf")},
timeout=60,
)
print(resp.json()) - 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 -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"
}' const res = await fetch("https://hubvocal.com/v1/knowledge/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"query": "how much is a scale and polish"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.post(
"https://hubvocal.com/v1/knowledge/search",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"query": "how much is a scale and polish",
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) 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).
{
"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 -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_…"
}' const res = await fetch("https://hubvocal.com/v1/mcp-servers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "crm",
"url": "https://mcp.example.com/mcp",
"auth": "bearer",
"secret": "sk_live_…"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.post(
"https://hubvocal.com/v1/mcp-servers",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"name": "crm",
"url": "https://mcp.example.com/mcp",
"auth": "bearer",
"secret": "sk_live_…",
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) - 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
writepassano 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 mcp add --transport http hubvocal https://hubvocal.com/mcp \
--header "Authorization: Bearer vt_live_…" {
"mcpServers": {
"hubvocal": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://hubvocal.com/mcp",
"--header",
"Authorization:${VT_AUTH}"
],
"env": {
"VT_AUTH": "Bearer vt_live_…"
}
}
}
} {
"mcpServers": {
"hubvocal": {
"url": "https://hubvocal.com/mcp",
"headers": {
"Authorization": "Bearer vt_live_…"
}
}
}
} from openai import OpenAI
client = OpenAI()
resp = client.responses.create(
model="gpt-5.6-luna",
tools=[{
"type": "mcp",
"server_label": "hubvocal",
"server_url": "https://hubvocal.com/mcp",
"headers": {"Authorization": "Bearer vt_live_…"},
"require_approval": "always",
}],
input="Summarise yesterday's calls.",
)
print(resp.output_text) 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
| Strumento | Scope | Cosa fa |
|---|---|---|
list_agents | read | Elenca i tuoi agenti. |
get_agent | read | Legge un agente. |
list_calls | read | Elenca le chiamate, filtrate per since, outcome o agent_id (fino a 50). |
get_call | read | Legge una chiamata con trascrizione e riepilogo. |
list_numbers | read | Elenca i tuoi numeri di telefono. |
list_knowledge | read | Elenca i documenti della knowledge base. |
search_knowledge | read | Cerca nella knowledge base. |
get_usage | read | Costi e minuti del mese in corso. |
create_agent | write | Crea un agente. |
update_agent | write | Modifica la bozza di un agente. |
publish_agent | write | Pubblica la bozza di un agente. |
add_knowledge_text | write | Aggiunge un documento di testo alla knowledge base. |
start_outbound_call | calls | Avvia 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>:
<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.
{
"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.
{
"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.
{
"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 -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"
]
}' const res = await fetch("https://hubvocal.com/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://crm.example.com/hooks/voice",
"events": [
"call.ended"
]
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.detail}`);
console.log(data); import os
import requests
resp = requests.post(
"https://hubvocal.com/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['VT_API_KEY']}"},
json={
"url": "https://crm.example.com/hooks/voice",
"events": [
"call.ended",
],
},
timeout=30,
)
if not resp.ok:
raise RuntimeError(f"{resp.status_code}: {resp.json()['detail']}")
print(resp.json()) L'evento
data è lo stesso oggetto chiamata restituito da GET /v1/calls/{room}, con la trascrizione e il riepilogo:
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:
- dopo 1 minuto
- dopo 5 minuti
- dopo 30 minuti
- 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.
// 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);
}); # Verifica prima la firma (stesso codice degli strumenti).
@app.post("/hooks/voice")
def voice_hook():
if not verify(request):
abort(401)
# Consegna già elaborata? Conferma la ricezione e fermati.
delivery = request.headers["X-VT-Delivery"]
if db.deliveries.exists(delivery):
return "", 200
db.deliveries.insert(delivery)
# Rispondi subito; il lavoro lento fallo in background.
event = json.loads(request.get_data())
queue.enqueue("call_ended", event["data"])
return "", 200 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 1 | Gemini Live | OpenAI Realtime | Classico |
|---|---|---|---|---|---|
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.
{
"detail": "Monthly spending limit reached"
} | Codice | Significato | Quando |
|---|---|---|
| 400 | Richiesta non valida | Non è 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). |
| 401 | Non autorizzato | Manca l'header Authorization, la chiave non inizia con vt_live_ oppure è stata revocata. |
| 402 | Limite raggiunto | L'organizzazione ha raggiunto il limite di spesa mensile; non è possibile avviare nuove chiamate. |
| 403 | Sospesa | L'organizzazione è sospesa; non è possibile avviare nuove chiamate. |
| 404 | Non trovato | Agente, chiamata, numero o endpoint inesistente. |
| 409 | Conflitto | POST /v1/agents è stato inviato con un id già esistente. |
| 429 | Troppe richieste | Più di 120 richieste al minuto con la stessa chiave. Attendi i secondi indicati in Retry-After. |
| 502 | Errore del fornitore | Il fornitore di telefonia ha rifiutato la chiamata in uscita o non ha risposto. |
| 500 | Errore del server | Si è 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.
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;
}