Разработчикам
Управляйте агентами, номерами и звонками из своих систем и добавьте голосовой звонок на свой сайт.
Обзор
Это REST API. Тела запросов и ответов — JSON (UTF-8), время — в секундах unix, суммы — в USD. Базовый URL:
https://hubvocal.com/v1
API-ключ принадлежит одной организации и действует в ней с правами администратора. Всё, что можно сделать в панели управления, можно сделать через API:
| Ресурс | Эндпоинты |
|---|---|
| Агенты | /v1/agents, /v1/agents/{id} |
| База знаний | /v1/knowledge, /v1/knowledge/{id}, /v1/knowledge/search |
| Инструменты | /v1/tools, /v1/tools/{id}, /v1/tools/{id}/test |
| MCP-серверы | /v1/mcp-servers, /v1/mcp-servers/{id}, /v1/mcp-servers/{id}/sync |
| Webhooks | /v1/webhooks, /v1/webhooks/{id}, /v1/webhooks/{id}/deliveries |
| Голоса и движки | /v1/voices |
| Веб-звонки | /v1/web-calls |
| Звонки и исходящие звонки | /v1/calls, /v1/calls/{room} |
| Номера | /v1/numbers, /v1/numbers/{number} |
| Потребление | /v1/usage |
Машиночитаемый документ OpenAPI 3.1 доступен по GET /v1/openapi.json, ключ не нужен. Подробности по каждому эндпоинту — в справочнике API.
Быстрый старт
-
Создайте API-ключ
В панели управления откройте Настройки > API-ключи и дайте ключу название, по которому понятно его назначение. Создавать ключи могут только владельцы и администраторы организации. Секрет (
vt_live_…) показывается один раз — сразу сохраните его в менеджере секретов.export VT_API_KEY="vt_live_…" -
Получите список агентов
Самый быстрый способ проверить, что ключ работает. Id из ответа понадобится на следующих шагах.
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()) -
Создайте агента
Движок, язык, голос и инструкции в одном объекте. Допустимые имена голосов и лимиты возвращает
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()) -
Начните веб-звонок
Подключение из браузера выполняется с
server_urlиtokenиз ответа; подробности ниже.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())
Аутентификация
Передавайте ключ как Bearer-токен в заголовке Authorization в каждом запросе:
Authorization: Bearer vt_live_… - Ключ действует в вашей организации с правами администратора: создаёт агентов, назначает номера и совершает звонки. Обращайтесь с ним как с паролем.
- Никогда не размещайте ключ в коде браузера, мобильном приложении или репозитории. Для звонков из браузера получайте короткоживущий токен на своём сервере.
- В списке Настройки > API-ключи видно, когда каждый ключ использовался последний раз. Отозванный ключ перестаёт работать сразу.
- Используйте отдельный ключ для каждой интеграции, чтобы отзыв одного не ломал остальные.
Веб-звонок на сайте
Посетители могут поговорить с агентом прямо на вашем сайте, в браузере. Это три шага:
- Посетитель нажимает кнопку, и страница обращается к вашему серверу.
- Ваш сервер вызывает
POST /v1/web-callsс API-ключом и возвращает ответ (server_url,token,room,max_seconds) на страницу. - Страница подключается к комнате через
livekit-clientи включает микрофон. Агент подключается сам и произносит приветствие.
1. Сервер: получите токен
// server.js (Node 18+, Express)
import express from "express";
const app = express();
app.post("/voice/session", async (req, res) => {
// Здесь добавьте свою аутентификацию и ограничение частоты запросов.
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():
# Здесь добавьте свою аутентификацию и ограничение частоты запросов.
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. Браузер: подключитесь
Если вы используете сборщик, установите npm install livekit-client@2.23.0 и импортируйте те же имена. Пример ниже загружает зафиксированную версию с CDN.
<button id="talk" type="button">Начать разговор</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;
// Ваш сервер возвращает ответ 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();
// Когда придёт звук агента, подключите его к элементу <audio>.
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 = "Начать разговор";
});
await room.connect(session.server_url, session.token);
await room.localParticipant.setMicrophoneEnabled(true);
button.disabled = false;
button.textContent = "Завершить";
});
</script> Начинайте подключение по действию пользователя (клику): браузеры привязывают к нему доступ к микрофону и автовоспроизведение звука. Страница должна открываться по HTTPS.
Звонок длится не дольше max_seconds, затем агент завершает его. После окончания получите карточку звонка через GET /v1/calls/{room}.
Исходящие звонки
Позвоните на номер с одного из номеров вашей организации; агент заговорит, когда трубку возьмут. Номер from должен быть в GET /v1/numbers. Если agent_id не указан, используется агент, назначенный на этот номер. Номера — в формате 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()) Ответ
{
"room": "tel-out-9c41e0b7",
"provider": "didww",
"sip_call_id": "SCL_7mVt2kQb9xRa"
} Звонок определяется по room из ответа. Когда звонок завершится, его карточка (расшифровка, стоимость, кто положил трубку) доступна через GET /v1/calls/{room}. Звонки без ответа завершаются со значением ended_by, равным no_answer.
Входящие звонки
На входящие звонки на ваши номера отвечает агент, назначенный на номер. Номера подключаются через DIDWW, Twilio или ваш SIP-транк (Asterisk и другие), в организацию их добавляет команда платформы. Какой агент отвечает, выбираете вы:
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()) Версии и безопасная публикация
Изменения агента попадают в черновик. Звонки на ваши номера, веб-звонки и исходящие звонки используют опубликованную версию, поэтому с черновиком можно работать, не затрагивая реальные звонки.
- Измените агента через
PUT /v1/agents/{id}. Меняется только черновик;draft_changedстановится true. - Проверьте черновик: начните веб-звонок с
version: "draft". - Опубликуйте через
POST /v1/agents/{id}/publish, при желании с заметкой. Черновик становится новой версией со своим номером, и звонки начинают использовать её. - Если что-то пошло не так, вызовите
POST /v1/agents/{id}/rollbackс номером одной из прежних версий: она будет опубликована снова, как новая версия.
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 и POST /v1/calls принимают version: "live" (по умолчанию), "draft" или номер версии. Агент, который ни разу не публиковался, работает по черновику.
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"
}' Проверка до публикации
Тестовая лаборатория проводит текстовые разговоры между имитацией абонента и вашим агентом, чтобы вы могли проверить изменение до того, как его услышат реальные абоненты.
- Напишите сценарий: кто звонит (
persona), чего он хочет (goal) и до 20 реплик. - Добавьте правила, которым должен соответствовать хороший звонок (
expect), например «спрашивает имя абонента перед записью». - Задайте примерные результаты для инструментов (
mocks); реальные инструменты и webhooks не вызываются. - Запустите сценарии на черновике. Отдельная модель проверяет каждое правило и объясняет, почему оно выполнено или нет.
{
"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"
} Разговоры идут в текстовом режиме, поэтому проверяются инструкции и использование инструментов, а не голос или задержка. Симуляции запускаются из панели управления; эндпоинта /v1 для них пока нет.
Инструменты
Инструменты позволяют агенту действовать во время звонка. Одно и то же описание работает на всех четырёх движках; в GPT-Live их выполняет фоновая модель. Инструменты включаются для каждого агента в поле tools.
Встроенные инструменты
| Имя | Что делает |
|---|---|
end_call | Агент прощается и кладёт трубку через 0,75 с после того, как договорит. Если абонент за это время заговорит, завершение звонка отменяется. В карточке звонка будет ended_by: "agent". |
take_message | Принимает name, phone, message и callback_requested и записывает их в outcomes[] карточки звонка. Подтверждения не запрашивает. |
search_knowledge | Добавляется автоматически, если у агента есть документы в knowledge. Ищет по ним и записывает источники в карточку звонка. |
check_availability | Тип read. Возвращает свободные слоты между date_from и date_to в пределах рабочих часов агента. Добавляется, если задан calendar. |
book_appointment | Тип write, проходит шаг подтверждения. Бронирует время start на имя name и телефон phone, при желании с заметкой note. Запись попадает в outcomes[]. |
cancel_appointment | Тип write, проходит шаг подтверждения. Отменяет запись по её booking_ref. |
Инструменты-webhooks
Инструмент-webhook — это HTTPS-эндпоинт на вашей стороне. Создайте его один раз для всей организации в панели управления в разделе Инструменты или через POST /v1/tools, затем добавьте его id в поле tools.webhooks агента (до 16 на агента).
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: строчные латинские буквы, цифры и подчёркивания, от 3 до 41 символа, начинается с буквы и уникально в организации. Именаend_callиtake_messageзарезервированы.descriptionобъясняет модели, когда использовать инструмент. Пишите его как поручение коллеге.parameters— объект JSON Schema, до 20 полей типа string, number, integer или boolean (можно enum).kind—readилиwrite;timeout_s— от 2 до 15 секунд.
Какой запрос мы отправляем
Когда модель вызывает инструмент, API отправляет POST на ваш URL. В теле — имя инструмента, аргументы, выбранные моделью, и немного контекста о звонке:
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"
}
} Каждый запрос подписывается секретом инструмента. X-VT-Timestamp — время unix; X-VT-Signature — sha256=, за которым следует HMAC-SHA256 от timestamp + "." + body в hex. Проверяйте подпись по сырому телу до разбора и отклоняйте метки времени старше нескольких минут.
Проверка подписи
// server.js (Node 18+, Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.VT_TOOL_SECRET;
// Подпись считается по точным байтам: читайте сырое тело.
app.post("/hooks/order", express.raw({ type: "*/*" }), (req, res) => {
const ts = req.get("X-VT-Timestamp") ?? "";
const sig = req.get("X-VT-Signature") ?? "";
// Отклоняйте старые запросы (защита от повторной отправки).
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"));
// Отвечайте коротко: до модели доходят только первые 4 КБ.
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", "")
# Отклоняйте старые запросы (защита от повторной отправки).
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(401)
# Подпись считается по точным байтам: читайте сырое тело.
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"]
# Отвечайте коротко: до модели доходят только первые 4 КБ.
return jsonify(order_id=args["order_id"], status="shipped", eta="2026-11-07") Ваш ответ
Ответьте со статусом 2xx и JSON или простым текстом. До модели доходят только первые 4 КБ, поэтому возвращайте лишь то, что агенту нужно сказать. Статус не из диапазона 2xx или тайм-аут сообщают модели, что инструмент не сработал, и агент учтёт это в разговоре.
Инструменты чтения и записи
- read-инструменты выполняются сразу — для справок вроде статуса заказа или часов работы.
- write-инструменты проходят шаг подтверждения. Первый вызов не выполняется: агент зачитывает абоненту детали и просит ответить «да» или «нет». Инструмент сработает, только если в следующей реплике абонента есть явное «да» на языке звонка.
- Один и тот же вызов (имя и аргументы) выполняется не больше одного раза за разговор.
Безопасность
- URL должны начинаться с
https://. Локальные адреса и адреса частных сетей отклоняются. - Секрет хранится в зашифрованном виде, и API никогда его не возвращает (
has_secretпоказывает, задан ли он). Агент его не видит: запрос выполняет API. - Аргументы и ответы не сохраняются. В карточке звонка остаются
tools[]с именем, типом, результатом и длительностью, а на временной шкале — дорожка TOOL.
Через POST /v1/tools/{id}/test можно отправить настоящий запрос с примерными аргументами и увидеть статус, время выполнения и тело ответа.
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()) База знаний
В базе знаний хранятся документы вашей организации. Если агенту выбраны документы, он получает инструмент search_knowledge и отвечает на основе найденного. Агенту предписано не придумывать то, чего в документах нет.
Добавить документ
Отправьте файл как multipart (file), обычный текст как JSON (title, text) или одну веб-страницу как JSON (url). Обработка идёт в фоне: сначала у документа статус processing, затем ready или 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()) - Файлы: pdf, docx, txt, md или html, до 10 МБ каждый.
- До 200 документов и 200 МБ в сумме на организацию.
- URL: одна страница, только https; локальные и частные адреса отклоняются.
Подключить к агенту
Перечислите id документов в knowledge.doc_ids агента (до 50). Перед запуском проверьте поиск через POST /v1/knowledge/search.
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()) В карточке звонка каждый поиск виден в tools[] как search_knowledge вместе с источниками sources (документ, заголовок и фрагмент).
Календарь и запись
Когда календарь подключён, агент во время звонка может проверить свободное время, записать абонента или отменить запись.
Календари подключаются в панели управления, в разделе Интеграции (Google Calendar). Эндпоинта /v1 для интеграций нет: в API вы только указываете агенту, какую из них использовать.
Настройки агента
Задайте объект calendar агента: интеграция и календарь, длительность приёма (15–240 мин), перерыв между приёмами (0–60 мин), часовой пояс, рабочие часы, минимальный срок до записи (0–72 ч) и на сколько дней вперёд можно записаться (1–90).
{
"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
}
} После этого агент получает три встроенных инструмента: check_availability (read), book_appointment и cancel_appointment (оба write). Операции write проходят шаг подтверждения: агент зачитывает время, имя и телефон и записывает только после явного «да».
Каждая запись сохраняется в карточке звонка в outcomes[] в виде {kind: "appointment", start, name, phone, ref}.
MCP
HubVocal поддерживает Model Context Protocol двумя способами: ваши агенты могут пользоваться инструментами ваших собственных MCP-серверов, а MCP-клиенты — управлять платформой.
Подключение MCP-сервера к агентам
Добавьте сервер через POST /v1/mcp-servers или в панели управления. Мы загрузим список его инструментов (tools/list); новые инструменты появляются выключенными. Включите нужные, отметьте каждый как read или write и перечислите их в поле tools.mcp агента.
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()) - Соединение устанавливает API, а не агент, поэтому агент никогда не видит секрет вашего сервера.
- URL должны быть https; локальные и частные адреса отклоняются, в том числе в момент подключения.
- Инструменты
writeпроходят шаг подтверждения: агент зачитывает детали и запускает инструмент только после однозначного «да», не больше одного раза за звонок. - На каждый вызов отводится 10 секунд; до модели доходит не больше 4 КБ результата.
- До 10 серверов на организацию и до 50 инструментов на сервер. MCP-инструменты входят в общий лимит агента — 16 инструментов.
В разговоре и в карточке звонка MCP-инструмент называется <server>.<tool>, например crm.find_customer.
Управление платформой из MCP-клиента
HubVocal сам тоже работает как MCP-сервер по адресу https://hubvocal.com/mcp (streamable HTTP). Для аутентификации передавайте API-ключ организации как Bearer-токен. Лимит запросов общий с /v1.
Подключение клиента
Замените vt_live_… своим ключом. Выдавайте клиенту ключ только с теми областями доступа, которые ему нужны.
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) В приложении ChatGPT собственные MCP-коннекторы добавляются в режиме разработчика; можно ли передать API-ключ в заголовке, зависит от вашего тарифа и настроек коннектора. Через OpenAI API ключ можно передать напрямую, как показано ниже.
Инструменты
| Инструмент | Область доступа | Что делает |
|---|---|---|
list_agents | read | Список ваших агентов. |
get_agent | read | Данные одного агента. |
list_calls | read | Список звонков с фильтром по since, outcome или agent_id (до 50). |
get_call | read | Звонок с расшифровкой и резюме. |
list_numbers | read | Список ваших номеров телефонов. |
list_knowledge | read | Список документов базы знаний. |
search_knowledge | read | Поиск по базе знаний. |
get_usage | read | Стоимость и минуты за текущий месяц. |
create_agent | write | Создать агента. |
update_agent | write | Изменить черновик агента. |
publish_agent | write | Опубликовать черновик агента. |
add_knowledge_text | write | Добавить текстовый документ в базу знаний. |
start_outbound_call | calls | Совершить исходящий звонок (agent_id, to). Действуют обычные правила и лимиты для исходящих звонков. |
Ресурсы и промпты
agent://{id}: настройки агента.call://{id}: расшифровка звонка в виде текста.- Промпт
daily_call_report: сводка звонков за день.
Области доступа и журнал аудита
У каждого API-ключа есть области доступа; их выбирают при создании ключа в панели управления:
read: инструменты и эндпоинты только для чтения.write: дополнительно создание и изменение агентов и документов.calls: дополнительно исходящие звонки.
У ключей, созданных до появления областей доступа, есть все три. Каждый вызов MCP-инструмента записывается в журнал аудита: ключ, инструмент, время и результат.
Веб-виджет
Виджет добавляет на ваш сайт кнопку звонка. Посетители разговаривают с вашим агентом прямо в браузере; серверный код с вашей стороны не нужен.
Создайте публичный ключ
В панели управления откройте агента и создайте публичный ключ (pk_…). Публичный ключ принадлежит одному агенту, и его безопасно размещать на странице: он разрешает только веб-звонки с этим агентом и только с разрешённых вами доменов.
- Разрешённые домены (
allowed_origins): звонки с других сайтов отклоняются. - Ограничение длительности каждого звонка.
- Не более 10 звонков в час с одного IP-адреса.
Добавьте скрипт
Вставьте этот код перед закрывающим тегом </body>:
<script src="https://hubvocal.com/widget.js" data-agent="pk_…" async></script> Цвет, расположение и текст кнопки (на языке посетителя) берутся из настроек widget агента: color, position, label.
{
"widget": {
"color": "#2563eb",
"position": "bottom-right",
"label": "Talk to us"
}
} Виджет вызывает POST /api/public/web-calls с публичным ключом; ваш API-ключ он никогда не видит. Публичными ключами вы управляете в панели управления.
Структурированные поля в CRM
Структурированные поля превращают разговор в данные, с которыми могут работать ваши системы: например, дату записи на приём, товар, о котором спрашивали, или просьбу перезвонить.
Задайте поля
Добавьте до 24 полей в post_call.fields агента. У каждого поля есть name (строчные латинские буквы, цифры и подчёркивания), type (string, number, boolean, enum или date), description, которое подсказывает модели, что искать, и при необходимости значения enum и 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?"
}
]
}
} После звонка поля заполняет та же модель, что пишет резюме. Значения, которые не прозвучали в разговоре, равны null. Вы получаете их как extracted в GET /v1/calls/{room} и в списке звонков, а также в теле webhook call.ended.
{
"extracted": {
"appointment_date": "2026-11-12",
"service": "cleaning",
"callback": true
}
} В экспорте CSV каждое поле идёт отдельным столбцом.
Webhooks звонков
Webhook отправляет в вашу систему событие call.ended, когда звонок завершён и его карточка, включая резюме, готова. С его помощью можно создать заявку, обновить контакт в CRM или отправить клиенту сообщение после звонка.
Если оценка качества звонков включена, можно также подписаться на call.quality_low. Он отправляется один раз за звонок, когда оценка опускается ниже порога вашей организации.
Добавьте до 5 webhooks на организацию в панели управления или через POST /v1/webhooks. Если вы не передадите secret, он будет сгенерирован и вернётся только в этом ответе. URL должен быть https; локальные и частные адреса отклоняются.
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()) Событие
data — тот же объект звонка, что возвращает GET /v1/calls/{room}, вместе с расшифровкой и резюме:
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
}
]
}
} Запросы подписываются точно так же, как запросы инструментов: X-VT-Timestamp и X-VT-Signature. Добавляются ещё два заголовка: X-VT-Event (тип события) и X-VT-Delivery (уникальный id этой доставки).
Повторные попытки
Доставка считается успешной, если в течение 10 секунд пришёл любой ответ 2xx. Иначе мы повторяем попытку:
- через 1 минуту
- через 5 минут
- через 30 минут
- через 2 часа
После 5 попыток в сумме доставка помечается как failed. Все попытки видны в GET /v1/webhooks/{id}/deliveries, а неудачную доставку можно снова поставить в очередь через POST /v1/webhooks/deliveries/{id}/retry.
Обработка дублей
Повторная попытка может прийти, даже если ваш предыдущий ответ просто потерялся по дороге, поэтому одно и то же событие иногда приходит несколько раз. Сохраняйте id из X-VT-Delivery и пропускайте те, что уже обработали. Отвечайте быстро, а долгую работу выполняйте в фоне.
// Сначала проверьте подпись (тот же код, что и для инструментов).
app.post("/hooks/voice", express.raw({ type: "*/*" }), async (req, res) => {
if (!verify(req)) return res.sendStatus(401);
// Эта доставка уже обработана? Подтвердите получение и выходите.
const delivery = req.get("X-VT-Delivery");
if (await db.deliveries.exists(delivery)) return res.sendStatus(200);
await db.deliveries.insert(delivery);
// Отвечайте быстро; долгую работу выполняйте в фоне.
res.sendStatus(200);
const event = JSON.parse(req.body.toString("utf8"));
queue.add("call-ended", event.data);
}); # Сначала проверьте подпись (тот же код, что и для инструментов).
@app.post("/hooks/voice")
def voice_hook():
if not verify(request):
abort(401)
# Эта доставка уже обработана? Подтвердите получение и выходите.
delivery = request.headers["X-VT-Delivery"]
if db.deliveries.exists(delivery):
return "", 200
db.deliveries.insert(delivery)
# Отвечайте быстро; долгую работу выполняйте в фоне.
event = json.loads(request.get_data())
queue.enqueue("call_ended", event["data"])
return "", 200 POST /v1/webhooks/{id}/test отправляет тестовое событие call.ended и возвращает статус, время ответа и тело ответа.
Поддерживаемые языки общения
На каких языках каждый движок может вести разговор. Язык агента задаётся полем lang с кодами из таблицы ниже.
- Поддерживается
- Экспериментально Движок официально не указывает этот язык. Он работает по инструкции, но качество мы не измеряли.
| Язык | lang | GPT-Live 1 | Gemini Live | OpenAI Realtime | Классический |
|---|---|---|---|---|---|
Türkçetr | tr | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Englishen | en | Поддерживается | Поддерживается | Экспериментально | Поддерживается |
Deutschde | de | Поддерживается | Поддерживается | Экспериментально | Поддерживается |
Françaisfr | fr | Поддерживается | Поддерживается | Экспериментально | Поддерживается |
Españoles | es | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Italianoit | it | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Portuguêspt | pt | Поддерживается | Поддерживается | Экспериментально | Поддерживается |
Nederlandsnl | nl | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Русскийru | ru | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Українськаuk | uk | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Polskipl | pl | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Românăro | ro | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Ελληνικάel | el | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Българскиbg | bg | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Češtinacs | cs | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Slovenčinask | sk | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Magyarhu | hu | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Hrvatskihr | hr | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Српскиsr | sr | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Bosanskibs | bs | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Slovenščinasl | sl | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Македонскиmk | mk | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Svenskasv | sv | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Danskda | da | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Norskno | no | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Suomifi | fi | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Eestiet | et | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Latviešulv | lv | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Lietuviųlt | lt | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Catalàca | ca | Экспериментально | Поддерживается | Экспериментально | Поддерживается |
Экспериментальные языки лучше сначала проверить в браузере, а потом подключать к телефонной линии.
Лимиты и ошибки
Лимит — 120 запросов в минуту на ключ. При превышении вы получите 429 с заголовком Retry-After в секундах. Ошибки приходят с HTTP-кодом и всегда с одинаковым телом; detail — сообщение для человека.
{
"detail": "Monthly spending limit reached"
} | Код | Значение | Когда |
|---|---|---|
| 400 | Неверный запрос | Тело не удалось прочитать, не хватает обязательного поля или значение недопустимо (например, номер не в формате E.164 или попытка удалить последнего агента). |
| 401 | Не авторизован | Нет заголовка Authorization, ключ не начинается с vt_live_ или отозван. |
| 402 | Лимит исчерпан | Организация достигла месячного лимита расходов; новые звонки не начинаются. |
| 403 | Заблокировано | Работа организации приостановлена; новые звонки не начинаются. |
| 404 | Не найдено | Нет такого агента, звонка, номера или эндпоинта. |
| 409 | Конфликт | POST /v1/agents отправлен с уже существующим id. |
| 429 | Слишком много запросов | Больше 120 запросов в минуту по одному ключу. Подождите столько секунд, сколько указано в Retry-After. |
| 502 | Ошибка провайдера | Провайдер телефонии отклонил исходящий звонок или не ответил. |
| 500 | Ошибка сервера | Произошло что-то непредвиденное. Повторите запрос; если ошибка не исчезает, напишите нам. |
Повторяйте только ответы 429 и 5xx. Код 4xx означает, что ошибка в самом запросе: повторная отправка результат не изменит.
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;
}