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:

RessourceEndpunkte
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

  1. 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.

    shell
    export VT_API_KEY="vt_live_…"
  2. Agenten auflisten

    Der schnellste Weg, den Schlüssel zu prüfen. Eine id aus der Antwort brauchen Sie in den nächsten Schritten.

    curl
    curl "https://hubvocal.com/v1/agents" \
      -H "Authorization: Bearer $VT_API_KEY"
  3. Agenten erstellen

    Engine, Sprache, Stimme und Anweisungen in einem Objekt. Gültige Stimmennamen und Limits liefert GET /v1/voices.

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

    Mit server_url und token aus der Antwort verbinden Sie sich aus dem Browser; Details unten.

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

Authentifizierung

Senden Sie den Schlüssel bei jeder Anfrage als Bearer-Token im Header Authorization:

HTTP
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:

  1. Der Besucher klickt auf eine Schaltfläche, und Ihre Seite ruft Ihren eigenen Server auf.
  2. Ihr Server ruft POST /v1/web-calls mit dem API-Schlüssel auf und gibt die Antwort (server_url, token, room, max_seconds) an die Seite zurück.
  3. Die Seite verbindet sich mit livekit-client mit dem Raum und schaltet das Mikrofon ein. Der Agent tritt selbstständig bei und spricht seine Begrüßung.

1. Server: Token holen

Node (Express)
// 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);

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.

index.html
<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
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"
}'

Antwort

JSON
{
  "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
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"
}'

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.

  1. Bearbeiten Sie den Agenten mit PUT /v1/agents/{id}. Das ändert nur den Entwurf; draft_changed wird true.
  2. Testen Sie den Entwurf: Starten Sie einen Web-Anruf mit version: "draft".
  3. Veröffentlichen Sie mit POST /v1/agents/{id}/publish, optional mit einer Notiz. Der Entwurf wird zu einer neuen, nummerierten Version und geht live.
  4. Stimmt etwas nicht, veröffentlicht POST /v1/agents/{id}/rollback mit einer früheren Versionsnummer diese Version erneut, als neue Version.
curl
curl -X POST "https://hubvocal.com/v1/agents/front-desk/publish" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Shorter greeting"
}'

POST /v1/web-calls und POST /v1/calls akzeptieren version: "live" (Standard), "draft" oder eine Versionsnummer. Ein Agent, der noch nie veröffentlicht wurde, verwendet seinen Entwurf.

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

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.

  1. Schreiben Sie ein Szenario: wer anruft (persona), was die Person möchte (goal) und bis zu 20 Gesprächszüge.
  2. Fügen Sie Regeln hinzu, die ein guter Anruf erfüllen muss (expect), zum Beispiel „fragt vor der Buchung nach dem Namen des Anrufers“.
  3. Geben Sie Beispielergebnisse für Tools vor (mocks); es wird kein echtes Tool und kein Webhook aufgerufen.
  4. 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.
JSON
{
  "scenarios": [
    {
      "name": "Booking without a name",
      "persona": "A caller in a hurry who gives short answers",
      "goal": "Book an appointment tomorrow afternoon",
      "max_turns": 12,
      "expect": [
        "Asks for the caller's name before booking",
        "Reads the time back and asks for confirmation"
      ],
      "mocks": {
        "check_availability": {
          "ok": true,
          "slots": [
            "2026-11-06T14:00",
            "2026-11-06T15:30"
          ]
        }
      }
    }
  ],
  "version": "draft"
}

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

NameFunktion
end_callDer 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_messageNimmt name, phone, message und callback_requested auf und schreibt sie in outcomes[] des Anrufprotokolls. Eine Bestätigung wird nicht abgefragt.
search_knowledgeWird automatisch hinzugefügt, wenn dem Agenten in knowledge Dokumente zugeordnet sind. Durchsucht sie und hält die Quellen im Anrufprotokoll fest.
check_availabilityLesend. Liefert freie Zeitfenster zwischen date_from und date_to innerhalb der Arbeitszeiten des Agenten. Wird hinzugefügt, wenn calendar gesetzt ist.
book_appointmentSchreibend, mit Bestätigungsschritt. Bucht start für name und phone, optional mit einer note. Die Buchung landet in outcomes[].
cancel_appointmentSchreibend, 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
curl -X POST "https://hubvocal.com/v1/tools" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 8,
  "enabled": true,
  "secret": "whsec_2b7f…"
}'
  • name: Kleinbuchstaben, Ziffern und Unterstriche, 3 bis 41 Zeichen, beginnend mit einem Buchstaben, eindeutig in der Organisation. end_call und take_message sind reserviert.
  • description sagt dem Modell, wann es das Tool verwenden soll. Formulieren Sie sie wie eine Anweisung an eine Kollegin oder einen Kollegen.
  • parameters ist ein JSON-Schema-Objekt mit bis zu 20 Feldern vom Typ string, number, integer oder boolean (enum erlaubt).
  • kind ist read oder write; timeout_s liegt 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:

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

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

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

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

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

// 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);

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_secret zeigt, 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
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"
  }
}'

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
curl -X POST "https://hubvocal.com/v1/knowledge" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -F "file=@price-list-2026.pdf"
  • 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
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"
}'

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).

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

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
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_…"
}'
  • 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 Code
claude mcp add --transport http hubvocal https://hubvocal.com/mcp \
  --header "Authorization: Bearer vt_live_…"

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

ToolScopeFunktion
list_agentsreadIhre Agenten auflisten.
get_agentreadEinen Agenten abrufen.
list_callsreadAnrufe auflisten, gefiltert nach since, outcome oder agent_id (bis zu 50).
get_callreadEinen Anruf mit Transkript und Zusammenfassung abrufen.
list_numbersreadIhre Rufnummern auflisten.
list_knowledgereadDokumente der Wissensdatenbank auflisten.
search_knowledgereadDie Wissensdatenbank durchsuchen.
get_usagereadKosten und Minuten des laufenden Monats.
create_agentwriteEinen Agenten anlegen.
update_agentwriteDen Entwurf eines Agenten ändern.
publish_agentwriteDen Entwurf eines Agenten veröffentlichen.
add_knowledge_textwriteEin Textdokument zur Wissensdatenbank hinzufügen.
start_outbound_callcallsEinen 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:

HTML
<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.

JSON
{
  "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.

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

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.

JSON
{
  "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
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"
  ]
}'

Das Ereignis

data ist dasselbe Anrufobjekt, das GET /v1/calls/{room} zurückgibt, mit Transkript und Zusammenfassung:

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

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

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:

  1. nach 1 Minute
  2. nach 5 Minuten
  3. nach 30 Minuten
  4. 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.

Node (Express)
// 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);
});

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 1Gemini LiveOpenAI RealtimeKlassisch
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.

402
{
  "detail": "Monthly spending limit reached"
}
CodeBedeutungWann
400Ungültige AnfrageDer 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).
401Nicht autorisiertKein Authorization-Header, der Schlüssel beginnt nicht mit vt_live_ oder wurde widerrufen.
402Limit erreichtDie Organisation hat ihr monatliches Ausgabenlimit erreicht; neue Anrufe sind nicht möglich.
403GesperrtDie Organisation ist gesperrt; neue Anrufe sind nicht möglich.
404Nicht gefundenAgent, Anruf, Rufnummer oder Endpunkt existiert nicht.
409KonfliktPOST /v1/agents wurde mit einer bereits vorhandenen id gesendet.
429Rate-LimitMehr als 120 Anfragen pro Minute mit einem Schlüssel. Warten Sie die in Retry-After angegebenen Sekunden ab.
502AnbieterfehlerDer Telefonieanbieter hat den ausgehenden Anruf abgelehnt oder nicht geantwortet.
500ServerfehlerEtwas 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.

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