Entwickler
Verwalten Sie Agenten, Rufnummern und Anrufe aus Ihren eigenen Systemen und bringen Sie Sprachanrufe auf Ihre Website.
Überblick
Die API ist eine REST-API. Anfrage- und Antwortkörper sind JSON (UTF-8), Zeitstempel sind Unix-Sekunden und Beträge in USD. Die Basis-URL lautet:
https://hubvocal.com/v1
Ein API-Schlüssel gehört zu genau einer Organisation und hat dort Admin-Rechte. Alles, was Sie im Dashboard tun können, geht auch über die API:
| Ressource | Endpunkte |
|---|---|
| Agenten | /v1/agents, /v1/agents/{id} |
| Wissensdatenbank | /v1/knowledge, /v1/knowledge/{id}, /v1/knowledge/search |
| Tools | /v1/tools, /v1/tools/{id}, /v1/tools/{id}/test |
| MCP-Server | /v1/mcp-servers, /v1/mcp-servers/{id}, /v1/mcp-servers/{id}/sync |
| Webhooks | /v1/webhooks, /v1/webhooks/{id}, /v1/webhooks/{id}/deliveries |
| Stimmen und Engines | /v1/voices |
| Web-Anrufe | /v1/web-calls |
| Anrufe und ausgehende Anrufe | /v1/calls, /v1/calls/{room} |
| Rufnummern | /v1/numbers, /v1/numbers/{number} |
| Nutzung | /v1/usage |
Ein maschinenlesbares OpenAPI-3.1-Dokument steht unter GET /v1/openapi.json bereit, ohne Schlüssel. Alle Endpunkte im Detail finden Sie in der API-Referenz.
Schnellstart
-
API-Schlüssel erstellen
Öffnen Sie im Dashboard Einstellungen > API-Schlüssel und geben Sie dem Schlüssel einen Namen, der seinen Zweck beschreibt. Nur Inhaber und Admins einer Organisation können Schlüssel erstellen. Das Secret (
vt_live_…) wird nur einmal angezeigt; legen Sie es sofort in einem Secrets-Manager ab.export VT_API_KEY="vt_live_…" -
Agenten auflisten
Der schnellste Weg, den Schlüssel zu prüfen. Eine id aus der Antwort brauchen Sie in den nächsten Schritten.
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()) -
Agenten erstellen
Engine, Sprache, Stimme und Anweisungen in einem Objekt. Gültige Stimmennamen und Limits liefert
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()) -
Web-Anruf starten
Mit
server_urlundtokenaus der Antwort verbinden Sie sich aus dem Browser; Details unten.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())
Authentifizierung
Senden Sie den Schlüssel bei jeder Anfrage als Bearer-Token im Header Authorization:
Authorization: Bearer vt_live_… - Der Schlüssel hat Admin-Rechte in Ihrer Organisation: Er erstellt Agenten, weist Rufnummern zu und startet Anrufe. Behandeln Sie ihn wie ein Passwort.
- Legen Sie den Schlüssel nie in Browser-Code, eine mobile App oder ein Repository. Für Browser-Anrufe holen Sie auf Ihrem Server ein kurzlebiges Token.
- Die Liste unter Einstellungen > API-Schlüssel zeigt, wann jeder Schlüssel zuletzt verwendet wurde. Ein widerrufener Schlüssel funktioniert sofort nicht mehr.
- Verwenden Sie pro Integration einen eigenen Schlüssel, damit ein Widerruf die anderen nicht beeinträchtigt.
Web-Anruf einbetten
Ihre Besucher können direkt auf Ihrer Website im Browser mit dem Agenten sprechen. Der Ablauf hat drei Schritte:
- Der Besucher klickt auf eine Schaltfläche, und Ihre Seite ruft Ihren eigenen Server auf.
- Ihr Server ruft
POST /v1/web-callsmit dem API-Schlüssel auf und gibt die Antwort (server_url,token,room,max_seconds) an die Seite zurück. - Die Seite verbindet sich mit
livekit-clientmit dem Raum und schaltet das Mikrofon ein. Der Agent tritt selbstständig bei und spricht seine Begrüßung.
1. Server: Token holen
// server.js (Node 18+, Express)
import express from "express";
const app = express();
app.post("/voice/session", async (req, res) => {
// Hier Ihre eigene Authentifizierung und Ratenbegrenzung einbauen.
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():
# Hier Ihre eigene Authentifizierung und Ratenbegrenzung einbauen.
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: verbinden
Wenn Sie einen Bundler nutzen, installieren Sie npm install livekit-client@2.23.0 und importieren dieselben Namen. Das Beispiel unten lädt eine feste Version von einem CDN.
<button id="talk" type="button">Gespräch starten</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;
// Ihr Server gibt die Antwort von POST /v1/web-calls unverändert zurück.
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();
// Audio des Agenten bei Eingang an ein <audio>-Element hängen.
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 = "Gespräch starten";
});
await room.connect(session.server_url, session.token);
await room.localParticipant.setMicrophoneEnabled(true);
button.disabled = false;
button.textContent = "Auflegen";
});
</script> Starten Sie die Verbindung durch eine Nutzeraktion (einen Klick): Browser knüpfen Mikrofonfreigabe und automatische Audiowiedergabe daran. Die Seite muss über HTTPS ausgeliefert werden.
Ein Anruf dauert höchstens max_seconds, danach legt der Agent auf. Nach dem Ende rufen Sie das Protokoll mit GET /v1/calls/{room} ab.
Ausgehende Anrufe
Rufen Sie eine Nummer von einer Rufnummer Ihrer Organisation an; der Agent spricht, sobald abgenommen wird. Die from-Nummer muss in GET /v1/numbers stehen. Ohne agent_id wird der Agent verwendet, der dieser Rufnummer zugewiesen ist. Rufnummern im E.164-Format (+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()) Antwort
{
"room": "tel-out-9c41e0b7",
"provider": "didww",
"sip_call_id": "SCL_7mVt2kQb9xRa"
} Das Feld room in der Antwort identifiziert den Anruf. Nach Gesprächsende ist das Protokoll (Transkript, Kosten, wer aufgelegt hat) über GET /v1/calls/{room} abrufbar. Unbeantwortete Anrufe enden mit ended_by gleich no_answer.
Eingehende Anrufe
Eingehende Anrufe auf Ihren Rufnummern nimmt der Agent an, der der jeweiligen Nummer zugewiesen ist. Die Rufnummern kommen über DIDWW, Twilio oder Ihren eigenen SIP-Trunk (Asterisk und andere) und werden vom Plattform-Team Ihrer Organisation hinzugefügt. Sie legen fest, welcher Agent abnimmt:
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()) Versionen und sicheres Veröffentlichen
Wenn Sie einen Agenten bearbeiten, ändern Sie seinen Entwurf. Anrufe auf Ihren Rufnummern, Web-Anrufe und ausgehende Anrufe verwenden die veröffentlichte Version. So arbeiten Sie am Entwurf, ohne den Live-Betrieb zu beeinträchtigen.
- Bearbeiten Sie den Agenten mit
PUT /v1/agents/{id}. Das ändert nur den Entwurf;draft_changedwird true. - Testen Sie den Entwurf: Starten Sie einen Web-Anruf mit
version: "draft". - Veröffentlichen Sie mit
POST /v1/agents/{id}/publish, optional mit einer Notiz. Der Entwurf wird zu einer neuen, nummerierten Version und geht live. - Stimmt etwas nicht, veröffentlicht
POST /v1/agents/{id}/rollbackmit einer früheren Versionsnummer diese Version erneut, als neue 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 und POST /v1/calls akzeptieren version: "live" (Standard), "draft" oder eine Versionsnummer. Ein Agent, der noch nie veröffentlicht wurde, verwendet seinen Entwurf.
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"
}' Vor dem Veröffentlichen testen
Das Testlabor führt Textgespräche zwischen einem simulierten Anrufer und Ihrem Agenten, damit Sie eine Änderung prüfen können, bevor echte Anrufer sie hören.
- Schreiben Sie ein Szenario: wer anruft (
persona), was die Person möchte (goal) und bis zu 20 Gesprächszüge. - Fügen Sie Regeln hinzu, die ein guter Anruf erfüllen muss (
expect), zum Beispiel „fragt vor der Buchung nach dem Namen des Anrufers“. - Geben Sie Beispielergebnisse für Tools vor (
mocks); es wird kein echtes Tool und kein Webhook aufgerufen. - Führen Sie die Szenarien mit dem Entwurf aus. Ein separates Modell prüft jede Regel und erklärt, warum sie erfüllt wurde oder nicht.
{
"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"
} Die Gespräche laufen im Textmodus. Getestet werden also Anweisungen und Tool-Nutzung, nicht Stimme oder Latenz. Simulationen starten Sie im Dashboard; einen /v1-Endpunkt dafür gibt es noch nicht.
Tools
Mit Tools kann der Agent während eines Anrufs handeln. Dieselbe Definition funktioniert mit allen vier Engines; bei GPT-Live führt das Hintergrundmodell sie aus. Tools schalten Sie pro Agent im Feld tools ein.
Integrierte Tools
| Name | Funktion |
|---|---|
end_call | Der Agent verabschiedet sich und legt 0,75 s nach dem Ende seiner Äußerung auf. Spricht der Anrufer in der Zwischenzeit, wird das Auflegen abgebrochen. Im Anrufprotokoll steht ended_by: "agent". |
take_message | Nimmt name, phone, message und callback_requested auf und schreibt sie in outcomes[] des Anrufprotokolls. Eine Bestätigung wird nicht abgefragt. |
search_knowledge | Wird automatisch hinzugefügt, wenn dem Agenten in knowledge Dokumente zugeordnet sind. Durchsucht sie und hält die Quellen im Anrufprotokoll fest. |
check_availability | Lesend. Liefert freie Zeitfenster zwischen date_from und date_to innerhalb der Arbeitszeiten des Agenten. Wird hinzugefügt, wenn calendar gesetzt ist. |
book_appointment | Schreibend, mit Bestätigungsschritt. Bucht start für name und phone, optional mit einer note. Die Buchung landet in outcomes[]. |
cancel_appointment | Schreibend, mit Bestätigungsschritt. Storniert eine Buchung anhand ihrer booking_ref. |
Webhook-Tools
Ein Webhook-Tool ist ein HTTPS-Endpunkt auf Ihrer Seite. Legen Sie es einmal für die Organisation an, im Dashboard unter Tools oder mit POST /v1/tools, und tragen Sie dann seine id in tools.webhooks eines Agenten ein (bis zu 16 pro 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: Kleinbuchstaben, Ziffern und Unterstriche, 3 bis 41 Zeichen, beginnend mit einem Buchstaben, eindeutig in der Organisation.end_callundtake_messagesind reserviert.descriptionsagt dem Modell, wann es das Tool verwenden soll. Formulieren Sie sie wie eine Anweisung an eine Kollegin oder einen Kollegen.parametersist ein JSON-Schema-Objekt mit bis zu 20 Feldern vom Typ string, number, integer oder boolean (enum erlaubt).kindistreadoderwrite;timeout_sliegt zwischen 2 und 15 Sekunden.
Die Anfrage, die wir senden
Ruft das Modell das Tool auf, sendet die API einen POST an Ihre URL. Der Body enthält den Tool-Namen, die vom Modell gewählten Argumente und etwas Kontext zum Anruf:
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"
}
} Jede Anfrage wird mit dem Secret des Tools signiert. X-VT-Timestamp ist die Unix-Zeit; X-VT-Signature ist sha256=, gefolgt vom hexadezimalen HMAC-SHA256 von timestamp + "." + body. Prüfen Sie die Signatur vor dem Parsen anhand des Roh-Bodys und lehnen Sie Zeitstempel ab, die älter als ein paar Minuten sind.
Signatur prüfen
// server.js (Node 18+, Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.VT_TOOL_SECRET;
// Die Signatur gilt für die exakten Bytes: den Roh-Body lesen.
app.post("/hooks/order", express.raw({ type: "*/*" }), (req, res) => {
const ts = req.get("X-VT-Timestamp") ?? "";
const sig = req.get("X-VT-Signature") ?? "";
// Alte Anfragen ablehnen (Schutz vor Replay-Angriffen).
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"));
// Antwort kurz halten: Nur die ersten 4 KB erreichen das Modell.
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", "")
# Alte Anfragen ablehnen (Schutz vor Replay-Angriffen).
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(401)
# Die Signatur gilt für die exakten Bytes: den Roh-Body lesen.
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"]
# Antwort kurz halten: Nur die ersten 4 KB erreichen das Modell.
return jsonify(order_id=args["order_id"], status="shipped", eta="2026-11-07") Ihre Antwort
Antworten Sie mit einem 2xx-Status und JSON oder reinem Text. Nur die ersten 4 KB erreichen das Modell, geben Sie also nur zurück, was der Agent sagen muss. Ein Status außerhalb von 2xx oder eine Zeitüberschreitung zeigt dem Modell, dass das Tool fehlgeschlagen ist, und der Agent geht im Gespräch damit um.
Lese- und Schreib-Tools
- read-Tools laufen sofort, etwa für Abfragen wie Bestellstatus oder Öffnungszeiten.
- write-Tools durchlaufen einen Bestätigungsschritt. Der erste Aufruf wird nicht ausgeführt; der Agent wiederholt die Angaben gegenüber dem Anrufer und fragt nach Ja oder Nein. Ausgeführt wird nur, wenn die nächste Äußerung des Anrufers ein klares Ja in der Sprache des Gesprächs enthält.
- Derselbe Aufruf (Name und Argumente) läuft höchstens einmal pro Gespräch.
Sicherheit
- URLs müssen
https://verwenden. Lokale und private Netzwerkadressen werden abgelehnt. - Das Secret wird verschlüsselt gespeichert und von der API nie zurückgegeben (
has_secretzeigt, ob eines gesetzt ist). Der Agent sieht es nie; die Anfrage stellt die API. - Argumente und Antworten werden nicht gespeichert. Das Anrufprotokoll enthält
tools[]mit Name, Art, Ergebnis und Dauer sowie eine TOOL-Spur auf der Zeitachse.
Mit POST /v1/tools/{id}/test senden Sie eine echte Anfrage mit Beispielargumenten und sehen Status, Dauer und Antwort-Body.
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()) Wissensdatenbank
Die Wissensdatenbank enthält die Dokumente Ihrer Organisation. Sind einem Agenten Dokumente zugeordnet, erhält er das Tool search_knowledge und antwortet auf Grundlage dessen, was er findet. Er ist angewiesen, nichts zu erfinden, was nicht in den Dokumenten steht.
Dokument hinzufügen
Senden Sie eine Datei als Multipart (file), reinen Text als JSON (title, text) oder eine einzelne Webseite als JSON (url). Die Verarbeitung läuft im Hintergrund: Das Dokument beginnt mit processing und wechselt dann zu ready oder 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()) - Dateien: pdf, docx, txt, md oder html, jeweils bis zu 10 MB.
- Bis zu 200 Dokumente und insgesamt 200 MB pro Organisation.
- URLs: eine einzelne Seite, nur https; lokale und private Adressen werden abgelehnt.
Einem Agenten zuordnen
Tragen Sie die Dokument-ids in knowledge.doc_ids des Agenten ein (bis zu 50). Probieren Sie Suchanfragen mit POST /v1/knowledge/search aus, bevor Sie live gehen.
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()) Im Anrufprotokoll erscheint jede Suche in tools[] als search_knowledge, mit ihren sources (Dokument, Titel und Abschnitt).
Kalender und Termine
Ist ein Kalender verbunden, kann der Agent während des Anrufs die Verfügbarkeit prüfen und Termine buchen oder absagen.
Kalender verbinden Sie im Dashboard unter Integrationen (Google Calendar). Für Integrationen gibt es keinen /v1-Endpunkt; über die API weisen Sie einem Agenten nur eine bestehende Integration zu.
Einstellungen des Agenten
Legen Sie das Objekt calendar des Agenten fest: Integration und Kalender, Termindauer (15–240 Min.), Puffer (0–60 Min.), Zeitzone, Arbeitszeiten, Mindestvorlauf (0–72 Std.) und wie weit im Voraus gebucht werden darf (1–90 Tage).
{
"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
}
} Der Agent erhält dann drei integrierte Tools: check_availability (read) sowie book_appointment und cancel_appointment (beide write). Schreibende Aufrufe durchlaufen den Bestätigungsschritt: Der Agent wiederholt Uhrzeit, Name und Telefonnummer und bucht erst nach einem klaren Ja.
Jede Buchung wird im Anrufprotokoll unter outcomes[] als {kind: "appointment", start, name, phone, ref} gespeichert.
MCP
HubVocal unterstützt das Model Context Protocol auf zwei Wegen: Ihre Agenten können Tools Ihrer eigenen MCP-Server nutzen, und MCP-Clients können die Plattform verwalten.
Einen MCP-Server mit Ihren Agenten verbinden
Fügen Sie Ihren Server mit POST /v1/mcp-servers oder im Dashboard hinzu. Wir rufen seine Tool-Liste ab (tools/list); neue Tools sind zunächst ausgeschaltet. Schalten Sie die gewünschten ein, markieren Sie jedes als read oder write und tragen Sie sie in tools.mcp des Agenten ein.
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()) - Die Verbindung baut die API auf, nicht der Agent. Das Secret Ihres Servers sieht der Agent daher nie.
- URLs müssen https verwenden; lokale und private Adressen werden abgelehnt, auch beim Verbindungsaufbau.
write-Tools durchlaufen den Bestätigungsschritt: Der Agent liest die Angaben vor und führt das Tool erst nach einem klaren Ja aus, höchstens einmal pro Anruf.- Jeder Tool-Aufruf bricht nach 10 Sekunden ab; höchstens 4 KB des Ergebnisses gelangen zum Modell.
- Bis zu 10 Server pro Organisation und 50 Tools pro Server. MCP-Tools zählen zum Limit von 16 Tools pro Agent.
Im Gespräch und im Anrufprotokoll heißt ein MCP-Tool <server>.<tool>, zum Beispiel crm.find_customer.
Die Plattform aus einem MCP-Client verwalten
HubVocal ist selbst auch ein MCP-Server unter https://hubvocal.com/mcp (Streamable HTTP). Sie authentifizieren sich mit einem API-Schlüssel der Organisation als Bearer-Token. Das Rate-Limit teilt sich der Server mit /v1.
Einen Client verbinden
Ersetzen Sie vt_live_… durch Ihren Schlüssel. Verwenden Sie einen Schlüssel, der nur die Scopes hat, die der Client braucht.
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) In der ChatGPT-App fügen Sie eigene MCP-Connectors im Entwicklermodus hinzu; ob Sie dabei einen Header mit API-Schlüssel verwenden können, hängt von Ihrem Tarif und den Connector-Einstellungen ab. Über die OpenAI API können Sie den Schlüssel direkt übergeben, wie unten gezeigt.
Tools
| Tool | Scope | Funktion |
|---|---|---|
list_agents | read | Ihre Agenten auflisten. |
get_agent | read | Einen Agenten abrufen. |
list_calls | read | Anrufe auflisten, gefiltert nach since, outcome oder agent_id (bis zu 50). |
get_call | read | Einen Anruf mit Transkript und Zusammenfassung abrufen. |
list_numbers | read | Ihre Rufnummern auflisten. |
list_knowledge | read | Dokumente der Wissensdatenbank auflisten. |
search_knowledge | read | Die Wissensdatenbank durchsuchen. |
get_usage | read | Kosten und Minuten des laufenden Monats. |
create_agent | write | Einen Agenten anlegen. |
update_agent | write | Den Entwurf eines Agenten ändern. |
publish_agent | write | Den Entwurf eines Agenten veröffentlichen. |
add_knowledge_text | write | Ein Textdokument zur Wissensdatenbank hinzufügen. |
start_outbound_call | calls | Einen ausgehenden Anruf starten (agent_id, to). Es gelten die üblichen Regeln und Limits für ausgehende Anrufe. |
Ressourcen und Prompts
agent://{id}: die Einstellungen eines Agenten.call://{id}: das Transkript eines Anrufs als Text.- Prompt
daily_call_report: eine Zusammenfassung der Anrufe des Tages.
Scopes und Audit-Log
Jeder API-Schlüssel hat Scopes, die Sie beim Anlegen im Dashboard auswählen:
read: Tools und Endpunkte, die nur lesen.write: zusätzlich Agenten und Dokumente anlegen und ändern.calls: zusätzlich ausgehende Anrufe starten.
Schlüssel, die vor Einführung der Scopes angelegt wurden, haben alle drei. Jeder MCP-Tool-Aufruf wird mit Schlüssel, Tool, Zeitpunkt und Ergebnis im Audit-Log festgehalten.
Web-Widget
Das Widget setzt einen Anruf-Button auf Ihre Website. Besucher sprechen im Browser mit Ihrem Agenten; auf Ihrer Seite ist kein Servercode nötig.
Öffentlichen Schlüssel anlegen
Öffnen Sie im Dashboard den Agenten und legen Sie einen öffentlichen Schlüssel (pk_…) an. Ein öffentlicher Schlüssel gehört zu genau einem Agenten und kann bedenkenlos in Ihre Seite eingebunden werden, denn er erlaubt nur Web-Anrufe mit diesem Agenten und nur von den Domains, die Sie freigeben.
- Erlaubte Domains (
allowed_origins): Anrufe von anderen Websites werden abgelehnt. - Ein Zeitlimit pro Anruf.
- Höchstens 10 Anrufe pro Stunde von derselben IP-Adresse.
Script einfügen
Fügen Sie dies vor dem schließenden </body>-Tag ein:
<script src="https://hubvocal.com/widget.js" data-agent="pk_…" async></script> Farbe, Position und Button-Text (in der Sprache des Besuchers) stammen aus den widget-Einstellungen des Agenten: color, position, label.
{
"widget": {
"color": "#2563eb",
"position": "bottom-right",
"label": "Talk to us"
}
} Das Widget ruft POST /api/public/web-calls mit dem öffentlichen Schlüssel auf; Ihren API-Schlüssel sieht es nie. Öffentliche Schlüssel verwalten Sie im Dashboard.
Strukturierte Felder ins CRM
Strukturierte Felder machen aus einem Gespräch Daten, mit denen Ihre Systeme arbeiten können, zum Beispiel das Termindatum, das angefragte Produkt oder ob ein Rückruf gewünscht ist.
Felder definieren
Fügen Sie bis zu 24 Felder zu post_call.fields des Agenten hinzu. Jedes hat einen name (Kleinbuchstaben, Ziffern und Unterstriche), einen type (string, number, boolean, enum oder date), eine description, die dem Modell sagt, wonach es suchen soll, und optional enum-Werte und 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?"
}
]
}
} Nach dem Anruf füllt dasselbe Modell, das die Zusammenfassung schreibt, die Felder aus. Nicht erwähnte Werte sind null. Sie erhalten sie als extracted in GET /v1/calls/{room} und in der Anrufliste sowie im Body des call.ended-Webhooks.
{
"extracted": {
"appointment_date": "2026-11-12",
"service": "cleaning",
"callback": true
}
} Der CSV-Export enthält jedes Feld als eigene Spalte.
Anruf-Webhooks
Ein Webhook liefert ein call.ended-Ereignis an Ihr System, sobald ein Anruf beendet und sein Protokoll samt Zusammenfassung vollständig ist. Damit legen Sie etwa ein Ticket an, aktualisieren einen CRM-Kontakt oder verschicken eine Nachfass-Nachricht.
Sind Qualitätswerte aktiviert, können Sie auch call.quality_low abonnieren. Das Ereignis wird einmal pro Anruf gesendet, wenn der Wert unter den Schwellenwert Ihrer Organisation fällt.
Legen Sie bis zu 5 Webhooks pro Organisation an, im Dashboard oder mit POST /v1/webhooks. Senden Sie kein secret, wird eines erzeugt und nur in dieser Antwort zurückgegeben. URLs müssen https verwenden; lokale und private Adressen werden abgelehnt.
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()) Das Ereignis
data ist dasselbe Anrufobjekt, das GET /v1/calls/{room} zurückgibt, mit Transkript und Zusammenfassung:
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
}
]
}
} Die Anfragen werden genauso signiert wie Tool-Anfragen: X-VT-Timestamp und X-VT-Signature. Hinzu kommen zwei Header: X-VT-Event (der Ereignistyp) und X-VT-Delivery (eine eindeutige id für diese Zustellung).
Erneute Versuche
Jede 2xx-Antwort innerhalb von 10 Sekunden gilt als zugestellt. Andernfalls versuchen wir es erneut:
- nach 1 Minute
- nach 5 Minuten
- nach 30 Minuten
- nach 2 Stunden
Nach insgesamt 5 Versuchen wird die Zustellung als failed markiert. Alle Versuche sehen Sie mit GET /v1/webhooks/{id}/deliveries; eine fehlgeschlagene Zustellung stellen Sie mit POST /v1/webhooks/deliveries/{id}/retry erneut in die Warteschlange.
Duplikate behandeln
Ein erneuter Versuch kann auch dann eintreffen, wenn Ihre frühere Antwort unterwegs verloren ging. Dasselbe Ereignis kann also mehrfach ankommen. Speichern Sie die id aus X-VT-Delivery und überspringen Sie ids, die Sie bereits verarbeitet haben. Antworten Sie schnell und erledigen Sie aufwendige Arbeit im Hintergrund.
// Zuerst die Signatur prüfen (derselbe Code wie bei Tools).
app.post("/hooks/voice", express.raw({ type: "*/*" }), async (req, res) => {
if (!verify(req)) return res.sendStatus(401);
// Zustellung schon verarbeitet? Bestätigen und abbrechen.
const delivery = req.get("X-VT-Delivery");
if (await db.deliveries.exists(delivery)) return res.sendStatus(200);
await db.deliveries.insert(delivery);
// Schnell antworten, Aufwendiges im Hintergrund erledigen.
res.sendStatus(200);
const event = JSON.parse(req.body.toString("utf8"));
queue.add("call-ended", event.data);
}); # Zuerst die Signatur prüfen (derselbe Code wie bei Tools).
@app.post("/hooks/voice")
def voice_hook():
if not verify(request):
abort(401)
# Zustellung schon verarbeitet? Bestätigen und abbrechen.
delivery = request.headers["X-VT-Delivery"]
if db.deliveries.exists(delivery):
return "", 200
db.deliveries.insert(delivery)
# Schnell antworten, Aufwendiges im Hintergrund erledigen.
event = json.loads(request.get_data())
queue.enqueue("call_ended", event["data"])
return "", 200 POST /v1/webhooks/{id}/test sendet ein Beispielereignis call.ended und gibt Status, Dauer und Antwort-Body zurück.
Unterstützte Gesprächssprachen
In welchen Sprachen jede Engine ein Gespräch führen kann. Die Sprache eines Agenten legen Sie mit dem Feld lang fest, mit den Codes unten.
- Unterstützt
- Experimentell Die Engine führt diese Sprache nicht offiziell. Sie funktioniert per Anweisung, die Qualität haben wir aber nicht gemessen.
| Sprache | lang | GPT-Live 1 | Gemini Live | OpenAI Realtime | Klassisch |
|---|---|---|---|---|---|
Türkçetr | tr | Experimentell | Unterstützt | Experimentell | Unterstützt |
Englishen | en | Unterstützt | Unterstützt | Experimentell | Unterstützt |
Deutschde | de | Unterstützt | Unterstützt | Experimentell | Unterstützt |
Françaisfr | fr | Unterstützt | Unterstützt | Experimentell | Unterstützt |
Españoles | es | Experimentell | Unterstützt | Experimentell | Unterstützt |
Italianoit | it | Experimentell | Unterstützt | Experimentell | Unterstützt |
Portuguêspt | pt | Unterstützt | Unterstützt | Experimentell | Unterstützt |
Nederlandsnl | nl | Experimentell | Unterstützt | Experimentell | Unterstützt |
Русскийru | ru | Experimentell | Unterstützt | Experimentell | Unterstützt |
Українськаuk | uk | Experimentell | Unterstützt | Experimentell | Unterstützt |
Polskipl | pl | Experimentell | Unterstützt | Experimentell | Unterstützt |
Românăro | ro | Experimentell | Unterstützt | Experimentell | Unterstützt |
Ελληνικάel | el | Experimentell | Unterstützt | Experimentell | Unterstützt |
Българскиbg | bg | Experimentell | Unterstützt | Experimentell | Unterstützt |
Češtinacs | cs | Experimentell | Unterstützt | Experimentell | Unterstützt |
Slovenčinask | sk | Experimentell | Unterstützt | Experimentell | Unterstützt |
Magyarhu | hu | Experimentell | Unterstützt | Experimentell | Unterstützt |
Hrvatskihr | hr | Experimentell | Unterstützt | Experimentell | Unterstützt |
Српскиsr | sr | Experimentell | Unterstützt | Experimentell | Unterstützt |
Bosanskibs | bs | Experimentell | Unterstützt | Experimentell | Unterstützt |
Slovenščinasl | sl | Experimentell | Unterstützt | Experimentell | Unterstützt |
Македонскиmk | mk | Experimentell | Unterstützt | Experimentell | Unterstützt |
Svenskasv | sv | Experimentell | Unterstützt | Experimentell | Unterstützt |
Danskda | da | Experimentell | Unterstützt | Experimentell | Unterstützt |
Norskno | no | Experimentell | Unterstützt | Experimentell | Unterstützt |
Suomifi | fi | Experimentell | Unterstützt | Experimentell | Unterstützt |
Eestiet | et | Experimentell | Unterstützt | Experimentell | Unterstützt |
Latviešulv | lv | Experimentell | Unterstützt | Experimentell | Unterstützt |
Lietuviųlt | lt | Experimentell | Unterstützt | Experimentell | Unterstützt |
Catalàca | ca | Experimentell | Unterstützt | Experimentell | Unterstützt |
Testen Sie experimentelle Sprachen im Browser, bevor Sie sie auf eine Telefonleitung legen.
Rate-Limits und Fehler
Das Rate-Limit liegt bei 120 Anfragen pro Minute pro Schlüssel. Darüber erhalten Sie 429 mit einem Retry-After-Header in Sekunden. Fehler kommen mit einem HTTP-Statuscode und immer demselben Body; detail ist eine lesbare Meldung.
{
"detail": "Monthly spending limit reached"
} | Code | Bedeutung | Wann |
|---|---|---|
| 400 | Ungültige Anfrage | Der Body ist nicht lesbar, ein Pflichtfeld fehlt oder ein Wert ist ungültig (etwa eine Nummer nicht in E.164 oder das Löschen des letzten Agenten). |
| 401 | Nicht autorisiert | Kein Authorization-Header, der Schlüssel beginnt nicht mit vt_live_ oder wurde widerrufen. |
| 402 | Limit erreicht | Die Organisation hat ihr monatliches Ausgabenlimit erreicht; neue Anrufe sind nicht möglich. |
| 403 | Gesperrt | Die Organisation ist gesperrt; neue Anrufe sind nicht möglich. |
| 404 | Nicht gefunden | Agent, Anruf, Rufnummer oder Endpunkt existiert nicht. |
| 409 | Konflikt | POST /v1/agents wurde mit einer bereits vorhandenen id gesendet. |
| 429 | Rate-Limit | Mehr als 120 Anfragen pro Minute mit einem Schlüssel. Warten Sie die in Retry-After angegebenen Sekunden ab. |
| 502 | Anbieterfehler | Der Telefonieanbieter hat den ausgehenden Anruf abgelehnt oder nicht geantwortet. |
| 500 | Serverfehler | Etwas Unerwartetes ist passiert. Versuchen Sie es erneut; bleibt der Fehler, schreiben Sie uns. |
Wiederholen Sie nur 429- und 5xx-Antworten. Ein 4xx bedeutet, dass mit der Anfrage selbst etwas nicht stimmt; erneutes Senden ändert nichts am Ergebnis.
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;
}