Développeurs
Gérez agents, numéros et appels depuis vos propres systèmes, et proposez des appels vocaux sur votre site web.
Présentation
L'API est une API REST. Les corps de requête et de réponse sont en JSON (UTF-8), les horodatages en secondes Unix et les montants en USD. L'URL de base est :
https://hubvocal.com/v1
Une clé API appartient à une seule organisation et y dispose des droits admin. Tout ce que vous faites dans le tableau de bord, vous pouvez le faire avec l'API :
| Ressource | Endpoints |
|---|---|
| Agents | /v1/agents, /v1/agents/{id} |
| Base de connaissances | /v1/knowledge, /v1/knowledge/{id}, /v1/knowledge/search |
| Outils | /v1/tools, /v1/tools/{id}, /v1/tools/{id}/test |
| Serveurs MCP | /v1/mcp-servers, /v1/mcp-servers/{id}, /v1/mcp-servers/{id}/sync |
| Webhooks | /v1/webhooks, /v1/webhooks/{id}, /v1/webhooks/{id}/deliveries |
| Voix et moteurs | /v1/voices |
| Appels web | /v1/web-calls |
| Appels et appels sortants | /v1/calls, /v1/calls/{room} |
| Numéros | /v1/numbers, /v1/numbers/{number} |
| Consommation | /v1/usage |
Un document OpenAPI 3.1 lisible par machine est disponible sur GET /v1/openapi.json, sans clé. Pour le détail de chaque endpoint, consultez la référence API.
Démarrage rapide
-
Créer une clé API
Dans le tableau de bord, ouvrez Paramètres > Clés API et donnez à la clé un nom qui indique son usage. Seuls les propriétaires et les admins d'une organisation peuvent créer des clés. Le secret (
vt_live_…) ne s'affiche qu'une fois ; stockez-le tout de suite dans un gestionnaire de secrets.export VT_API_KEY="vt_live_…" -
Lister vos agents
Le moyen le plus rapide de vérifier que la clé fonctionne. Vous utiliserez un id de la réponse aux étapes suivantes.
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()) -
Créer un agent
Moteur, langue, voix et instructions dans un seul objet. Pour les noms de voix valides et les limites, appelez
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()) -
Lancer un appel web
Vous vous connectez depuis le navigateur avec le
server_urlet letokende la réponse ; détails ci-dessous.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())
Authentification
Envoyez la clé comme jeton Bearer dans l'en-tête Authorization de chaque requête :
Authorization: Bearer vt_live_… - La clé dispose des droits admin dans votre organisation : elle crée des agents, associe des numéros et passe des appels. Traitez-la comme un mot de passe.
- Ne placez jamais la clé dans du code navigateur, une application mobile ou un dépôt. Pour les appels depuis le navigateur, obtenez un jeton à durée de vie courte sur votre serveur.
- La liste sous Paramètres > Clés API indique la dernière utilisation de chaque clé. Une clé révoquée cesse de fonctionner immédiatement.
- Utilisez une clé par intégration, pour qu'une révocation ne casse pas les autres.
Intégrer un appel web
Vos visiteurs peuvent parler à l'agent directement sur votre site, dans le navigateur. Le parcours se fait en trois étapes :
- Le visiteur clique sur un bouton et votre page appelle votre propre serveur.
- Votre serveur appelle
POST /v1/web-callsavec la clé API et renvoie la réponse (server_url,token,room,max_seconds) à la page. - La page se connecte à la salle avec
livekit-clientet active le micro. L'agent rejoint la salle de lui-même et prononce son message d'accueil.
1. Serveur : obtenir un jeton
// server.js (Node 18+, Express)
import express from "express";
const app = express();
app.post("/voice/session", async (req, res) => {
// Appliquez ici votre propre authentification et limitation de débit.
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():
# Appliquez ici votre propre authentification et limitation de débit.
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. Navigateur : se connecter
Si vous utilisez un bundler, installez npm install livekit-client@2.23.0 et importez les mêmes noms. L'exemple ci-dessous charge une version figée depuis un CDN.
<button id="talk" type="button">Commencer à parler</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;
// Votre serveur renvoie telle quelle la réponse de 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();
// Rattacher l'audio de l'agent à un élément <audio> dès son arrivée.
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 = "Commencer à parler";
});
await room.connect(session.server_url, session.token);
await room.localParticipant.setMicrophoneEnabled(true);
button.disabled = false;
button.textContent = "Raccrocher";
});
</script> Lancez la connexion à partir d'une action de l'utilisateur (un clic) : les navigateurs y lient l'autorisation du micro et la lecture automatique de l'audio. La page doit être servie en HTTPS.
Un appel dure au maximum max_seconds, puis l'agent raccroche. À la fin, récupérez sa fiche avec GET /v1/calls/{room}.
Appels sortants
Appelez un numéro depuis l'un des numéros de votre organisation ; l'agent parle dès que l'on décroche. Le numéro from doit figurer dans GET /v1/numbers. Sans agent_id, c'est l'agent associé à ce numéro qui est utilisé. Les numéros sont au format 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()) Réponse
{
"room": "tel-out-9c41e0b7",
"provider": "didww",
"sip_call_id": "SCL_7mVt2kQb9xRa"
} Le room de la réponse identifie l'appel. Une fois l'appel terminé, sa fiche (transcription, coût, qui a raccroché) est disponible via GET /v1/calls/{room}. Les appels sans réponse se terminent avec ended_by à no_answer.
Appels entrants
Les appels entrants sur vos numéros sont pris par l'agent associé à chaque numéro. Les numéros arrivent via DIDWW, Twilio ou votre propre trunk SIP (Asterisk et autres) et sont ajoutés à votre organisation par l'équipe plateforme. Vous choisissez quel agent répond :
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()) Versions et publication sécurisée
Modifier un agent modifie son brouillon. Les appels sur vos numéros, les appels web et les appels sortants utilisent la version publiée : vous pouvez donc travailler sur le brouillon sans toucher aux appels en production.
- Modifiez l'agent avec
PUT /v1/agents/{id}. Seul le brouillon change ;draft_changedpasse à true. - Testez le brouillon : lancez un appel web avec
version: "draft". - Publiez avec
POST /v1/agents/{id}/publish, en ajoutant une note si vous le souhaitez. Le brouillon devient une nouvelle version numérotée et passe en production. - En cas de problème,
POST /v1/agents/{id}/rollbackavec un numéro de version antérieur republie cette version sous la forme d'une nouvelle version.
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 et POST /v1/calls acceptent version : "live" (par défaut), "draft" ou un numéro de version. Un agent qui n'a jamais été publié utilise son brouillon.
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"
}' Tester avant de publier
Le laboratoire de test fait dialoguer par écrit un appelant simulé et votre agent, pour que vous puissiez vérifier une modification avant que de vrais appelants ne l'entendent.
- Rédigez un scénario : qui appelle (
persona), ce que la personne veut (goal) et jusqu'à 20 tours de parole. - Ajoutez les règles qu'un bon appel doit respecter (
expect), par exemple « demande le nom de l'appelant avant de réserver ». - Fournissez des résultats d'exemple pour les outils (
mocks) ; aucun outil ni webhook réel n'est appelé. - Exécutez les scénarios sur le brouillon. Un modèle distinct vérifie chaque règle et explique pourquoi elle est respectée ou non.
{
"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"
} Les conversations se déroulent en mode texte : elles testent les instructions et l'utilisation des outils, pas la voix ni la latence. Les simulations se lancent depuis le tableau de bord ; il n'existe pas encore d'endpoint /v1 pour elles.
Outils
Les outils permettent à l'agent d'agir pendant un appel. La même définition fonctionne sur les quatre moteurs ; avec GPT-Live, c'est le modèle en arrière-plan qui les exécute. Les outils s'activent par agent, dans le champ tools.
Outils intégrés
| Nom | Rôle |
|---|---|
end_call | L'agent dit au revoir et raccroche 0,75 s après avoir fini de parler. Si l'appelant reprend la parole entre-temps, le raccrochage est annulé. La fiche d'appel indique ended_by: "agent". |
take_message | Recueille name, phone, message et callback_requested et les enregistre dans outcomes[] de la fiche d'appel. Aucune confirmation n'est demandée. |
search_knowledge | Ajouté automatiquement lorsque l'agent dispose de documents dans knowledge. Effectue la recherche et enregistre les sources dans la fiche d'appel. |
check_availability | Lecture. Renvoie les créneaux libres entre date_from et date_to, dans les horaires de travail de l'agent. Ajouté lorsque calendar est défini. |
book_appointment | Écriture, soumise à l'étape de confirmation. Réserve start pour name et phone, avec une note facultative. La réservation est ajoutée à outcomes[]. |
cancel_appointment | Écriture, soumise à l'étape de confirmation. Annule une réservation à partir de sa booking_ref. |
Outils webhook
Un outil webhook est un endpoint HTTPS de votre côté. Définissez-le une fois pour l'organisation, dans le tableau de bord sous Outils ou avec POST /v1/tools, puis ajoutez son id à tools.webhooks d'un agent (jusqu'à 16 par agent).
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: lettres minuscules, chiffres et tirets bas, de 3 à 41 caractères, commençant par une lettre, unique dans l'organisation.end_callettake_messagesont réservés.descriptionindique au modèle quand utiliser l'outil. Rédigez-la comme une consigne donnée à un collègue.parametersest un objet JSON Schema comptant jusqu'à 20 champs de type string, number, integer ou boolean (enum autorisé).kindvautreadouwrite;timeout_sva de 2 à 15 secondes.
La requête que nous envoyons
Quand le modèle appelle l'outil, l'API envoie un POST à votre URL. Le corps contient le nom de l'outil, les arguments choisis par le modèle et quelques éléments de contexte sur l'appel :
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"
}
} Chaque requête est signée avec le secret de l'outil. X-VT-Timestamp est l'heure Unix ; X-VT-Signature vaut sha256= suivi du HMAC-SHA256 en hexadécimal de timestamp + "." + body. Vérifiez la signature sur le corps brut avant de l'analyser, et rejetez les horodatages de plus de quelques minutes.
Vérifier la signature
// 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 signature porte sur les octets exacts : lisez le corps brut.
app.post("/hooks/order", express.raw({ type: "*/*" }), (req, res) => {
const ts = req.get("X-VT-Timestamp") ?? "";
const sig = req.get("X-VT-Signature") ?? "";
// Rejeter les requêtes trop anciennes (protection contre le rejeu).
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"));
// Réponse courte : seuls les 4 premiers Ko parviennent au modèle.
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", "")
# Rejeter les requêtes trop anciennes (protection contre le rejeu).
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(401)
# La signature porte sur les octets exacts : lisez le corps brut.
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"]
# Réponse courte : seuls les 4 premiers Ko parviennent au modèle.
return jsonify(order_id=args["order_id"], status="shipped", eta="2026-11-07") Votre réponse
Répondez avec un statut 2xx et du JSON ou du texte brut. Seuls les 4 premiers Ko parviennent au modèle : renvoyez uniquement ce que l'agent doit dire. Un statut autre que 2xx ou un délai dépassé signale au modèle que l'outil a échoué, et l'agent gère la situation dans la conversation.
Outils de lecture et d'écriture
- read : ces outils s'exécutent immédiatement, pour des consultations comme le statut d'une commande ou les horaires d'ouverture.
- write : ces outils passent par une étape de confirmation. Le premier appel n'est pas exécuté ; l'agent relit les informations à l'appelant et lui demande de répondre par oui ou par non. L'outil ne s'exécute que si la prise de parole suivante de l'appelant contient un oui clair, dans la langue de l'appel.
- Un même appel (nom et arguments) s'exécute au plus une fois par conversation.
Sécurité
- Les URL doivent utiliser
https://. Les adresses locales et de réseau privé sont refusées. - Le secret est stocké chiffré et n'est jamais renvoyé par l'API (
has_secretindique s'il est défini). L'agent ne le voit jamais : c'est l'API qui envoie la requête. - Les arguments et les réponses ne sont pas conservés. La fiche d'appel garde
tools[]avec le nom, le type, le résultat et la durée, ainsi qu'une piste TOOL sur la chronologie.
Utilisez POST /v1/tools/{id}/test pour envoyer une vraie requête avec des arguments d'exemple et voir le statut, la durée et le corps de la réponse.
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()) Base de connaissances
La base de connaissances regroupe les documents de votre organisation. Lorsque des documents sont sélectionnés pour un agent, celui-ci reçoit l'outil search_knowledge et répond à partir de ce qu'il trouve. Il a pour consigne de ne pas inventer ce qui ne figure pas dans les documents.
Ajouter un document
Envoyez un fichier en multipart (file), du texte brut en JSON (title, text) ou une seule page web en JSON (url). Le traitement se fait en arrière-plan : le document démarre à l'état processing, puis passe à ready ou 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()) - Fichiers : pdf, docx, txt, md ou html, 10 Mo maximum chacun.
- Jusqu'à 200 documents et 200 Mo au total par organisation.
- URL : une seule page, en https uniquement ; les adresses locales et privées sont refusées.
L'attribuer à un agent
Indiquez les id des documents dans knowledge.doc_ids de l'agent (jusqu'à 50). Testez quelques recherches avec POST /v1/knowledge/search avant de passer en production.
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()) Dans la fiche d'appel, chaque recherche apparaît dans tools[] sous le nom search_knowledge, avec ses sources (document, titre et extrait).
Agenda et rendez-vous
Une fois un agenda connecté, l'agent peut vérifier les disponibilités et réserver ou annuler des rendez-vous pendant l'appel.
Les agendas se connectent dans le tableau de bord, sous Intégrations (Google Calendar). Il n'existe pas d'endpoint /v1 pour les intégrations : dans l'API, vous indiquez seulement à un agent laquelle utiliser.
Paramètres de l'agent
Définissez l'objet calendar de l'agent : intégration et agenda utilisés, durée des rendez-vous (15 à 240 min), temps tampon (0 à 60 min), fuseau horaire, horaires de travail, délai de prévenance minimal (0 à 72 h) et horizon de réservation (1 à 90 jours).
{
"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'agent reçoit alors trois outils intégrés : check_availability (read), book_appointment et cancel_appointment (tous deux write). Les écritures passent par l'étape de confirmation : l'agent relit l'heure, le nom et le numéro de téléphone, et ne réserve qu'après un oui clair.
Chaque réservation est enregistrée dans la fiche d'appel, sous outcomes[], au format {kind: "appointment", start, name, phone, ref}.
MCP
HubVocal prend en charge le Model Context Protocol de deux façons : vos agents peuvent utiliser les outils de vos propres serveurs MCP, et des clients MCP peuvent gérer la plateforme.
Connecter un serveur MCP à vos agents
Ajoutez votre serveur avec POST /v1/mcp-servers ou depuis le tableau de bord. Nous récupérons sa liste d'outils (tools/list) ; les nouveaux outils sont désactivés par défaut. Activez ceux que vous voulez, indiquez pour chacun read ou write, puis ajoutez-les au champ tools.mcp de l'agent.
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 connexion est établie par l'API, pas par l'agent : l'agent ne voit donc jamais le secret de votre serveur.
- Les URL doivent être en https ; les adresses locales et privées sont refusées, y compris au moment de la connexion.
- Les outils
writepassent par l'étape de confirmation : l'agent relit les informations et n'exécute l'outil qu'après un oui clair, une fois au plus par appel. - Chaque appel d'outil expire au bout de 10 secondes ; 4 Ko au plus du résultat sont transmis au modèle.
- Jusqu'à 10 serveurs par organisation et 50 outils par serveur. Les outils MCP comptent dans la limite de 16 outils par agent.
Dans la conversation et dans la fiche d'appel, un outil MCP s'appelle <server>.<tool>, par exemple crm.find_customer.
Gérer la plateforme depuis un client MCP
HubVocal est aussi un serveur MCP, à l'adresse https://hubvocal.com/mcp (HTTP streamable). Authentifiez-vous avec une clé API d'organisation, envoyée comme jeton Bearer. La limite de débit est partagée avec /v1.
Connecter un client
Remplacez vt_live_… par votre clé. Utilisez une clé qui n'a que les scopes dont le client a besoin.
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) Dans l'application ChatGPT, les connecteurs MCP personnalisés s'ajoutent en mode développeur ; la possibilité d'utiliser un en-tête avec une clé API dépend de votre forfait et des paramètres du connecteur. Avec l'API OpenAI, vous pouvez transmettre la clé directement, comme ci-dessous.
Outils
| Outil | Scope | Rôle |
|---|---|---|
list_agents | read | Lister vos agents. |
get_agent | read | Consulter un agent. |
list_calls | read | Lister les appels, filtrés par since, outcome ou agent_id (50 au plus). |
get_call | read | Consulter un appel avec sa transcription et son résumé. |
list_numbers | read | Lister vos numéros de téléphone. |
list_knowledge | read | Lister les documents de la base de connaissances. |
search_knowledge | read | Rechercher dans la base de connaissances. |
get_usage | read | Coût et minutes du mois en cours. |
create_agent | write | Créer un agent. |
update_agent | write | Modifier le brouillon d'un agent. |
publish_agent | write | Publier le brouillon d'un agent. |
add_knowledge_text | write | Ajouter un document texte à la base de connaissances. |
start_outbound_call | calls | Passer un appel sortant (agent_id, to). Les règles et limites habituelles des appels sortants s'appliquent. |
Ressources et prompts
agent://{id}: les paramètres d'un agent.call://{id}: la transcription d'un appel, en texte.- Prompt
daily_call_report: un résumé des appels de la journée.
Scopes et journal d'audit
Chaque clé API a des scopes, choisis lors de sa création dans le tableau de bord :
read: outils et endpoints en lecture seule.write: en plus, créer et modifier des agents et des documents.calls: en plus, passer des appels sortants.
Les clés créées avant l'introduction des scopes ont les trois. Chaque appel d'outil MCP est consigné dans le journal d'audit avec la clé, l'outil, l'heure et le résultat.
Widget web
Le widget ajoute un bouton d'appel à votre site web. Les visiteurs parlent à votre agent dans le navigateur ; aucun code serveur n'est nécessaire de votre côté.
Créer une clé publique
Dans le tableau de bord, ouvrez l'agent et créez une clé publique (pk_…). Une clé publique appartient à un seul agent et peut figurer dans votre page sans risque, car elle n'autorise que des appels web avec cet agent, et uniquement depuis les domaines que vous autorisez.
- Domaines autorisés (
allowed_origins) : les appels provenant d'autres sites sont refusés. - Une durée maximale par appel.
- Au plus 10 appels par heure depuis une même adresse IP.
Ajouter le script
Collez ceci avant la balise de fermeture </body> :
<script src="https://hubvocal.com/widget.js" data-agent="pk_…" async></script> La couleur, la position et le texte du bouton (dans la langue du visiteur) proviennent des paramètres widget de l'agent : color, position, label.
{
"widget": {
"color": "#2563eb",
"position": "bottom-right",
"label": "Talk to us"
}
} Le widget appelle POST /api/public/web-calls avec la clé publique ; il ne voit jamais votre clé API. Les clés publiques se gèrent dans le tableau de bord.
Champs structurés vers votre CRM
Les champs structurés transforment une conversation en données exploitables par vos systèmes, par exemple la date du rendez-vous, le produit demandé ou le souhait d'être rappelé.
Définir les champs
Ajoutez jusqu'à 24 champs dans post_call.fields de l'agent. Chacun a un name (lettres minuscules, chiffres et tirets bas), un type (string, number, boolean, enum ou date), une description qui indique au modèle ce qu'il doit chercher, et éventuellement des valeurs enum et 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?"
}
]
}
} Après l'appel, le modèle qui rédige le résumé remplit aussi les champs. Les valeurs non mentionnées valent null. Vous les recevez dans extracted via GET /v1/calls/{room} et la liste des appels, ainsi que dans le corps du webhook call.ended.
{
"extracted": {
"appointment_date": "2026-11-12",
"service": "cleaning",
"callback": true
}
} L'export CSV inclut chaque champ sous forme de colonne.
Webhooks d'appel
Un webhook envoie un événement call.ended à votre système dès qu'un appel est terminé et que sa fiche, résumé compris, est complète. Utilisez-le pour créer un ticket, mettre à jour un contact dans votre CRM ou envoyer un message de suivi.
Si les notes de qualité d'appel sont activées, vous pouvez aussi vous abonner à call.quality_low. Cet événement est envoyé une fois par appel quand la note passe sous le seuil de votre organisation.
Ajoutez jusqu'à 5 webhooks par organisation, dans le tableau de bord ou avec POST /v1/webhooks. Si vous n'envoyez pas de secret, il est généré et renvoyé uniquement dans cette réponse. Les URL doivent être en https ; les adresses locales et privées sont refusées.
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'événement
data est le même objet d'appel que celui renvoyé par GET /v1/calls/{room}, avec la transcription et le résumé :
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
}
]
}
} Les requêtes sont signées exactement comme les requêtes d'outils : X-VT-Timestamp et X-VT-Signature. Deux en-têtes s'y ajoutent : X-VT-Event (le type d'événement) et X-VT-Delivery (un id unique pour cette livraison).
Nouvelles tentatives
Toute réponse 2xx reçue en moins de 10 secondes vaut livraison. Sinon, nous réessayons :
- après 1 minute
- après 5 minutes
- après 30 minutes
- après 2 heures
Après 5 tentatives au total, la livraison est marquée failed. Vous voyez toutes les tentatives avec GET /v1/webhooks/{id}/deliveries et pouvez remettre une livraison échouée en file d'attente avec POST /v1/webhooks/deliveries/{id}/retry.
Gérer les doublons
Une nouvelle tentative peut arriver même si votre réponse précédente s'est perdue en route : le même événement peut donc arriver plusieurs fois. Conservez l'id X-VT-Delivery et ignorez ceux que vous avez déjà traités. Répondez vite et traitez les tâches longues en arrière-plan.
// Vérifiez d'abord la signature (même code que pour les outils).
app.post("/hooks/voice", express.raw({ type: "*/*" }), async (req, res) => {
if (!verify(req)) return res.sendStatus(401);
// Livraison déjà traitée ? Accusez réception et arrêtez-vous là.
const delivery = req.get("X-VT-Delivery");
if (await db.deliveries.exists(delivery)) return res.sendStatus(200);
await db.deliveries.insert(delivery);
// Répondez vite ; faites le travail long en arrière-plan.
res.sendStatus(200);
const event = JSON.parse(req.body.toString("utf8"));
queue.add("call-ended", event.data);
}); # Vérifiez d'abord la signature (même code que pour les outils).
@app.post("/hooks/voice")
def voice_hook():
if not verify(request):
abort(401)
# Livraison déjà traitée ? Accusez réception et arrêtez-vous là.
delivery = request.headers["X-VT-Delivery"]
if db.deliveries.exists(delivery):
return "", 200
db.deliveries.insert(delivery)
# Répondez vite ; faites le travail long en arrière-plan.
event = json.loads(request.get_data())
queue.enqueue("call_ended", event["data"])
return "", 200 POST /v1/webhooks/{id}/test envoie un exemple d'événement call.ended et renvoie le statut, la durée et le corps de la réponse.
Langues de conversation prises en charge
Les langues dans lesquelles chaque moteur peut mener une conversation. La langue d'un agent se définit avec le champ lang, à l'aide des codes ci-dessous.
- Prise en charge
- Expérimentale Le moteur ne liste pas officiellement cette langue. Elle fonctionne sur instruction, mais nous n'en avons pas mesuré la qualité.
| Langue | lang | GPT-Live 1 | Gemini Live | OpenAI Realtime | Classique |
|---|---|---|---|---|---|
Türkçetr | tr | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Englishen | en | Prise en charge | Prise en charge | Expérimentale | Prise en charge |
Deutschde | de | Prise en charge | Prise en charge | Expérimentale | Prise en charge |
Françaisfr | fr | Prise en charge | Prise en charge | Expérimentale | Prise en charge |
Españoles | es | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Italianoit | it | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Portuguêspt | pt | Prise en charge | Prise en charge | Expérimentale | Prise en charge |
Nederlandsnl | nl | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Русскийru | ru | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Українськаuk | uk | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Polskipl | pl | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Românăro | ro | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Ελληνικάel | el | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Българскиbg | bg | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Češtinacs | cs | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Slovenčinask | sk | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Magyarhu | hu | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Hrvatskihr | hr | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Српскиsr | sr | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Bosanskibs | bs | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Slovenščinasl | sl | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Македонскиmk | mk | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Svenskasv | sv | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Danskda | da | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Norskno | no | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Suomifi | fi | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Eestiet | et | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Latviešulv | lv | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Lietuviųlt | lt | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Catalàca | ca | Expérimentale | Prise en charge | Expérimentale | Prise en charge |
Testez les langues expérimentales dans le navigateur avant de les mettre sur une ligne téléphonique.
Limites et erreurs
La limite de débit est de 120 requêtes par minute et par clé. Au-delà, vous recevez 429 avec un en-tête Retry-After en secondes. Les erreurs arrivent avec un code de statut HTTP et toujours le même corps ; detail est un message lisible.
{
"detail": "Monthly spending limit reached"
} | Code | Signification | Quand |
|---|---|---|
| 400 | Requête invalide | Le corps est illisible, un champ obligatoire manque ou une valeur est invalide (par exemple un numéro hors E.164, ou la suppression du dernier agent). |
| 401 | Non autorisé | Pas d'en-tête Authorization, la clé ne commence pas par vt_live_, ou elle a été révoquée. |
| 402 | Plafond atteint | L'organisation a atteint son plafond de dépenses mensuel ; aucun nouvel appel ne peut démarrer. |
| 403 | Suspendue | L'organisation est suspendue ; aucun nouvel appel ne peut démarrer. |
| 404 | Introuvable | Agent, appel, numéro ou endpoint inexistant. |
| 409 | Conflit | POST /v1/agents a été envoyé avec un id qui existe déjà. |
| 429 | Limite de débit | Plus de 120 requêtes par minute sur une même clé. Attendez le nombre de secondes indiqué dans Retry-After. |
| 502 | Erreur fournisseur | Le fournisseur de téléphonie a refusé l'appel sortant ou n'a pas répondu. |
| 500 | Erreur serveur | Un problème inattendu est survenu. Réessayez ; si cela persiste, écrivez-nous. |
Ne réessayez que les réponses 429 et 5xx. Un 4xx signifie que la requête elle-même pose problème ; la renvoyer ne changera pas le résultat.
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;
}