Розробникам
Керуйте агентами, номерами й дзвінками зі своїх систем і додайте голосовий дзвінок на свій сайт.
Огляд
Це REST API. Тіла запитів і відповідей — JSON (UTF-8), час — у секундах unix, суми — у доларах США. Базова 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 |
| Вебхуки | /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); справжні інструменти й вебхуки не викликаються. - Запустіть сценарії на чернетці. Окрема модель перевіряє кожне правило й пояснює, чому його виконано чи ні.
{
"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. |
Вебхук-інструменти
Вебхук-інструмент — це 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. Перевіряйте підпис за сирим тілом ще до розбору JSON і відхиляйте запити з міткою часу, старшою за кілька хвилин.
Перевірка підпису
// 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} і в списку дзвінків, а також у тілі вебхука call.ended.
{
"extracted": {
"appointment_date": "2026-11-12",
"service": "cleaning",
"callback": true
}
} В експорті CSV кожне поле подано окремим стовпцем.
Вебхуки дзвінків
Вебхук надсилає у вашу систему подію call.ended, щойно дзвінок завершено, а його картку разом із підсумком сформовано. З його допомогою можна створити заявку, оновити контакт у CRM або надіслати клієнтові повідомлення після дзвінка.
Якщо оцінку якості дзвінків увімкнено, можна також підписатися на call.quality_low. Його надсилають один раз за дзвінок, коли оцінка падає нижче порогу вашої організації.
Додайте до 5 вебхуків на організацію в панелі керування або через 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;
}