Разработчикам

Управляйте агентами, номерами и звонками из своих систем и добавьте голосовой звонок на свой сайт.

Обзор

Это 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.

Быстрый старт

  1. Создайте API-ключ

    В панели управления откройте Настройки > API-ключи и дайте ключу название, по которому понятно его назначение. Создавать ключи могут только владельцы и администраторы организации. Секрет (vt_live_…) показывается один раз — сразу сохраните его в менеджере секретов.

    shell
    export VT_API_KEY="vt_live_…"
  2. Получите список агентов

    Самый быстрый способ проверить, что ключ работает. Id из ответа понадобится на следующих шагах.

    curl
    curl "https://hubvocal.com/v1/agents" \
      -H "Authorization: Bearer $VT_API_KEY"
  3. Создайте агента

    Движок, язык, голос и инструкции в одном объекте. Допустимые имена голосов и лимиты возвращает GET /v1/voices.

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

    Подключение из браузера выполняется с server_url и token из ответа; подробности ниже.

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

Аутентификация

Передавайте ключ как Bearer-токен в заголовке Authorization в каждом запросе:

HTTP
Authorization: Bearer vt_live_…
  • Ключ действует в вашей организации с правами администратора: создаёт агентов, назначает номера и совершает звонки. Обращайтесь с ним как с паролем.
  • Никогда не размещайте ключ в коде браузера, мобильном приложении или репозитории. Для звонков из браузера получайте короткоживущий токен на своём сервере.
  • В списке Настройки > API-ключи видно, когда каждый ключ использовался последний раз. Отозванный ключ перестаёт работать сразу.
  • Используйте отдельный ключ для каждой интеграции, чтобы отзыв одного не ломал остальные.

Веб-звонок на сайте

Посетители могут поговорить с агентом прямо на вашем сайте, в браузере. Это три шага:

  1. Посетитель нажимает кнопку, и страница обращается к вашему серверу.
  2. Ваш сервер вызывает POST /v1/web-calls с API-ключом и возвращает ответ (server_url, token, room, max_seconds) на страницу.
  3. Страница подключается к комнате через livekit-client и включает микрофон. Агент подключается сам и произносит приветствие.

1. Сервер: получите токен

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

2. Браузер: подключитесь

Если вы используете сборщик, установите npm install livekit-client@2.23.0 и импортируйте те же имена. Пример ниже загружает зафиксированную версию с CDN.

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

Ответ

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

Версии и безопасная публикация

Изменения агента попадают в черновик. Звонки на ваши номера, веб-звонки и исходящие звонки используют опубликованную версию, поэтому с черновиком можно работать, не затрагивая реальные звонки.

  1. Измените агента через PUT /v1/agents/{id}. Меняется только черновик; draft_changed становится true.
  2. Проверьте черновик: начните веб-звонок с version: "draft".
  3. Опубликуйте через POST /v1/agents/{id}/publish, при желании с заметкой. Черновик становится новой версией со своим номером, и звонки начинают использовать её.
  4. Если что-то пошло не так, вызовите POST /v1/agents/{id}/rollback с номером одной из прежних версий: она будет опубликована снова, как новая версия.
curl
curl -X POST "https://hubvocal.com/v1/agents/front-desk/publish" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Shorter greeting"
}'

POST /v1/web-calls и POST /v1/calls принимают version: "live" (по умолчанию), "draft" или номер версии. Агент, который ни разу не публиковался, работает по черновику.

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

Проверка до публикации

Тестовая лаборатория проводит текстовые разговоры между имитацией абонента и вашим агентом, чтобы вы могли проверить изменение до того, как его услышат реальные абоненты.

  1. Напишите сценарий: кто звонит (persona), чего он хочет (goal) и до 20 реплик.
  2. Добавьте правила, которым должен соответствовать хороший звонок (expect), например «спрашивает имя абонента перед записью».
  3. Задайте примерные результаты для инструментов (mocks); реальные инструменты и webhooks не вызываются.
  4. Запустите сценарии на черновике. Отдельная модель проверяет каждое правило и объясняет, почему оно выполнено или нет.
JSON
{
  "scenarios": [
    {
      "name": "Booking without a name",
      "persona": "A caller in a hurry who gives short answers",
      "goal": "Book an appointment tomorrow afternoon",
      "max_turns": 12,
      "expect": [
        "Asks for the caller's name before booking",
        "Reads the time back and asks for confirmation"
      ],
      "mocks": {
        "check_availability": {
          "ok": true,
          "slots": [
            "2026-11-06T14:00",
            "2026-11-06T15:30"
          ]
        }
      }
    }
  ],
  "version": "draft"
}

Разговоры идут в текстовом режиме, поэтому проверяются инструкции и использование инструментов, а не голос или задержка. Симуляции запускаются из панели управления; эндпоинта /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
curl -X POST "https://hubvocal.com/v1/tools" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "check_order",
  "description": "Looks up the shipping status of an order by its number.",
  "kind": "read",
  "url": "https://crm.example.com/hooks/order",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number"
      }
    },
    "required": [
      "order_id"
    ]
  },
  "timeout_s": 8,
  "enabled": true,
  "secret": "whsec_2b7f…"
}'
  • name: строчные латинские буквы, цифры и подчёркивания, от 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. В теле — имя инструмента, аргументы, выбранные моделью, и немного контекста о звонке:

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

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

Каждый запрос подписывается секретом инструмента. X-VT-Timestamp — время unix; X-VT-Signature — sha256=, за которым следует HMAC-SHA256 от timestamp + "." + body в hex. Проверяйте подпись по сырому телу до разбора и отклоняйте метки времени старше нескольких минут.

Проверка подписи

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

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

// Подпись считается по точным байтам: читайте сырое тело.
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);

Ваш ответ

Ответьте со статусом 2xx и JSON или простым текстом. До модели доходят только первые 4 КБ, поэтому возвращайте лишь то, что агенту нужно сказать. Статус не из диапазона 2xx или тайм-аут сообщают модели, что инструмент не сработал, и агент учтёт это в разговоре.

Инструменты чтения и записи

  • read-инструменты выполняются сразу — для справок вроде статуса заказа или часов работы.
  • write-инструменты проходят шаг подтверждения. Первый вызов не выполняется: агент зачитывает абоненту детали и просит ответить «да» или «нет». Инструмент сработает, только если в следующей реплике абонента есть явное «да» на языке звонка.
  • Один и тот же вызов (имя и аргументы) выполняется не больше одного раза за разговор.

Безопасность

  • URL должны начинаться с https://. Локальные адреса и адреса частных сетей отклоняются.
  • Секрет хранится в зашифрованном виде, и API никогда его не возвращает (has_secret показывает, задан ли он). Агент его не видит: запрос выполняет API.
  • Аргументы и ответы не сохраняются. В карточке звонка остаются tools[] с именем, типом, результатом и длительностью, а на временной шкале — дорожка TOOL.

Через POST /v1/tools/{id}/test можно отправить настоящий запрос с примерными аргументами и увидеть статус, время выполнения и тело ответа.

curl
curl -X POST "https://hubvocal.com/v1/tools/tl_8f3a2c/test" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "arguments": {
    "order_id": "A-10234"
  }
}'

База знаний

В базе знаний хранятся документы вашей организации. Если агенту выбраны документы, он получает инструмент search_knowledge и отвечает на основе найденного. Агенту предписано не придумывать то, чего в документах нет.

Добавить документ

Отправьте файл как multipart (file), обычный текст как JSON (title, text) или одну веб-страницу как JSON (url). Обработка идёт в фоне: сначала у документа статус processing, затем ready или failed.

curl
curl -X POST "https://hubvocal.com/v1/knowledge" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -F "file=@price-list-2026.pdf"
  • Файлы: pdf, docx, txt, md или html, до 10 МБ каждый.
  • До 200 документов и 200 МБ в сумме на организацию.
  • URL: одна страница, только https; локальные и частные адреса отклоняются.

Подключить к агенту

Перечислите id документов в knowledge.doc_ids агента (до 50). Перед запуском проверьте поиск через POST /v1/knowledge/search.

curl
curl -X POST "https://hubvocal.com/v1/knowledge/search" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "how much is a scale and polish"
}'

В карточке звонка каждый поиск виден в tools[] как search_knowledge вместе с источниками sources (документ, заголовок и фрагмент).

Календарь и запись

Когда календарь подключён, агент во время звонка может проверить свободное время, записать абонента или отменить запись.

Календари подключаются в панели управления, в разделе Интеграции (Google Calendar). Эндпоинта /v1 для интеграций нет: в API вы только указываете агенту, какую из них использовать.

Настройки агента

Задайте объект calendar агента: интеграция и календарь, длительность приёма (15–240 мин), перерыв между приёмами (0–60 мин), часовой пояс, рабочие часы, минимальный срок до записи (0–72 ч) и на сколько дней вперёд можно записаться (1–90).

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

После этого агент получает три встроенных инструмента: 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
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_…"
}'
  • Соединение устанавливает 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 Code
claude mcp add --transport http hubvocal https://hubvocal.com/mcp \
  --header "Authorization: Bearer vt_live_…"

В приложении ChatGPT собственные MCP-коннекторы добавляются в режиме разработчика; можно ли передать API-ключ в заголовке, зависит от вашего тарифа и настроек коннектора. Через OpenAI API ключ можно передать напрямую, как показано ниже.

Инструменты

ИнструментОбласть доступаЧто делает
list_agentsreadСписок ваших агентов.
get_agentreadДанные одного агента.
list_callsreadСписок звонков с фильтром по since, outcome или agent_id (до 50).
get_callreadЗвонок с расшифровкой и резюме.
list_numbersreadСписок ваших номеров телефонов.
list_knowledgereadСписок документов базы знаний.
search_knowledgereadПоиск по базе знаний.
get_usagereadСтоимость и минуты за текущий месяц.
create_agentwriteСоздать агента.
update_agentwriteИзменить черновик агента.
publish_agentwriteОпубликовать черновик агента.
add_knowledge_textwriteДобавить текстовый документ в базу знаний.
start_outbound_callcallsСовершить исходящий звонок (agent_id, to). Действуют обычные правила и лимиты для исходящих звонков.

Ресурсы и промпты

  • agent://{id}: настройки агента.
  • call://{id}: расшифровка звонка в виде текста.
  • Промпт daily_call_report: сводка звонков за день.

Области доступа и журнал аудита

У каждого API-ключа есть области доступа; их выбирают при создании ключа в панели управления:

  • read: инструменты и эндпоинты только для чтения.
  • write: дополнительно создание и изменение агентов и документов.
  • calls: дополнительно исходящие звонки.

У ключей, созданных до появления областей доступа, есть все три. Каждый вызов MCP-инструмента записывается в журнал аудита: ключ, инструмент, время и результат.

Веб-виджет

Виджет добавляет на ваш сайт кнопку звонка. Посетители разговаривают с вашим агентом прямо в браузере; серверный код с вашей стороны не нужен.

Создайте публичный ключ

В панели управления откройте агента и создайте публичный ключ (pk_…). Публичный ключ принадлежит одному агенту, и его безопасно размещать на странице: он разрешает только веб-звонки с этим агентом и только с разрешённых вами доменов.

  • Разрешённые домены (allowed_origins): звонки с других сайтов отклоняются.
  • Ограничение длительности каждого звонка.
  • Не более 10 звонков в час с одного IP-адреса.

Добавьте скрипт

Вставьте этот код перед закрывающим тегом </body>:

HTML
<script src="https://hubvocal.com/widget.js" data-agent="pk_…" async></script>

Цвет, расположение и текст кнопки (на языке посетителя) берутся из настроек widget агента: color, position, label.

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

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

После звонка поля заполняет та же модель, что пишет резюме. Значения, которые не прозвучали в разговоре, равны null. Вы получаете их как extracted в GET /v1/calls/{room} и в списке звонков, а также в теле webhook call.ended.

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

Событие

data — тот же объект звонка, что возвращает GET /v1/calls/{room}, вместе с расшифровкой и резюме:

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

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

Запросы подписываются точно так же, как запросы инструментов: X-VT-Timestamp и X-VT-Signature. Добавляются ещё два заголовка: X-VT-Event (тип события) и X-VT-Delivery (уникальный id этой доставки).

Повторные попытки

Доставка считается успешной, если в течение 10 секунд пришёл любой ответ 2xx. Иначе мы повторяем попытку:

  1. через 1 минуту
  2. через 5 минут
  3. через 30 минут
  4. через 2 часа

После 5 попыток в сумме доставка помечается как failed. Все попытки видны в GET /v1/webhooks/{id}/deliveries, а неудачную доставку можно снова поставить в очередь через POST /v1/webhooks/deliveries/{id}/retry.

Обработка дублей

Повторная попытка может прийти, даже если ваш предыдущий ответ просто потерялся по дороге, поэтому одно и то же событие иногда приходит несколько раз. Сохраняйте id из X-VT-Delivery и пропускайте те, что уже обработали. Отвечайте быстро, а долгую работу выполняйте в фоне.

Node (Express)
// Сначала проверьте подпись (тот же код, что и для инструментов).
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);
});

POST /v1/webhooks/{id}/test отправляет тестовое событие call.ended и возвращает статус, время ответа и тело ответа.

Поддерживаемые языки общения

На каких языках каждый движок может вести разговор. Язык агента задаётся полем lang с кодами из таблицы ниже.

  • Поддерживается
  • Экспериментально Движок официально не указывает этот язык. Он работает по инструкции, но качество мы не измеряли.
Язык lang GPT-Live 1Gemini LiveOpenAI 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 — сообщение для человека.

402
{
  "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 означает, что ошибка в самом запросе: повторная отправка результат не изменит.

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