Desarrolladores

Gestiona agentes, números y llamadas desde tus propios sistemas, y añade una llamada de voz a tu sitio web.

Resumen

Es una API REST. Los cuerpos de solicitud y respuesta son JSON (UTF-8), las marcas de tiempo van en segundos unix y los importes en USD. La URL base es:

https://hubvocal.com/v1

Una clave de API pertenece a una sola organización y actúa con permisos de administrador dentro de ella. Todo lo que puedes hacer en el panel, puedes hacerlo con la API:

RecursoEndpoints
Agentes/v1/agents, /v1/agents/{id}
Base de conocimiento/v1/knowledge, /v1/knowledge/{id}, /v1/knowledge/search
Herramientas/v1/tools, /v1/tools/{id}, /v1/tools/{id}/test
Servidores MCP/v1/mcp-servers, /v1/mcp-servers/{id}, /v1/mcp-servers/{id}/sync
Webhooks/v1/webhooks, /v1/webhooks/{id}, /v1/webhooks/{id}/deliveries
Voces y motores/v1/voices
Llamadas web/v1/web-calls
Llamadas y llamadas salientes/v1/calls, /v1/calls/{room}
Números/v1/numbers, /v1/numbers/{number}
Uso/v1/usage

Hay un documento OpenAPI 3.1 legible por máquina en GET /v1/openapi.json, y no necesita clave. Para ver cada endpoint en detalle, consulta la referencia de la API.

Inicio rápido

  1. Crea una clave de API

    En el panel, abre Configuración > Claves de API y ponle a la clave un nombre que indique para qué sirve. Solo los propietarios y administradores de la organización pueden crear claves. El secreto (vt_live_…) se muestra una sola vez; guárdalo enseguida en un gestor de secretos.

    shell
    export VT_API_KEY="vt_live_…"
  2. Lista tus agentes

    La forma más rápida de comprobar que la clave funciona. En los siguientes pasos usarás un id de la respuesta.

    curl
    curl "https://hubvocal.com/v1/agents" \
      -H "Authorization: Bearer $VT_API_KEY"
  3. Crea un agente

    Motor, idioma, voz e instrucciones en un solo objeto. Para ver los nombres de voz válidos y los límites, llama a 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. Inicia una llamada web

    Te conectas desde el navegador con el server_url y el token de la respuesta; tienes los detalles más abajo.

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

Autenticación

Envía la clave como token Bearer en el encabezado Authorization de cada solicitud:

HTTP
Authorization: Bearer vt_live_…
  • La clave actúa con permisos de administrador en tu organización: crea agentes, asigna números y hace llamadas. Trátala como una contraseña.
  • Nunca pongas la clave en código del navegador, en una app móvil ni en un repositorio. Para llamadas desde el navegador, obtén un token de corta duración en tu servidor.
  • La lista de Configuración > Claves de API muestra cuándo se usó cada clave por última vez. Una clave revocada deja de funcionar de inmediato.
  • Usa una clave por integración, para que revocar una no afecte a las demás.

Integrar una llamada web

Tus visitantes pueden hablar con el agente directamente en tu sitio, desde el navegador. El flujo tiene tres pasos:

  1. El visitante pulsa un botón y tu página llama a tu propio servidor.
  2. Tu servidor llama a POST /v1/web-calls con la clave de API y devuelve la respuesta (server_url, token, room, max_seconds) a la página.
  3. La página se conecta a la sala con livekit-client y activa el micrófono. El agente entra solo y dice su saludo.

1. Servidor: obtén un token

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

const app = express();

app.post("/voice/session", async (req, res) => {
  // Aplica aquí tu propia autenticación y limitación de solicitudes.
  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. Navegador: conéctate

Si usas un bundler, instala npm install livekit-client@2.23.0 e importa los mismos nombres. El ejemplo de abajo carga una versión fija desde un CDN.

index.html
<button id="talk" type="button">Empezar a hablar</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;
    // Tu servidor devuelve tal cual la respuesta de 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();
    // Conecta el audio del agente a un elemento <audio> cuando llegue.
    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 = "Empezar a hablar";
    });

    await room.connect(session.server_url, session.token);
    await room.localParticipant.setMicrophoneEnabled(true);
    button.disabled = false;
    button.textContent = "Colgar";
  });
</script>

Inicia la conexión a partir de un gesto del usuario (un clic): los navegadores vinculan a él el permiso del micrófono y la reproducción automática de audio. La página debe servirse por HTTPS.

Una llamada dura como máximo max_seconds; después, el agente cuelga. Cuando termine, obtén su registro con GET /v1/calls/{room}.

Llamadas salientes

Llama a un número desde uno de los números de tu organización; el agente habla en cuanto contestan. El número from debe aparecer en GET /v1/numbers. Sin agent_id, se usa el agente asignado a ese número. Los números van en formato 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"
}'

Respuesta

JSON
{
  "room": "tel-out-9c41e0b7",
  "provider": "didww",
  "sip_call_id": "SCL_7mVt2kQb9xRa"
}

El room de la respuesta identifica la llamada. Cuando la llamada termina, su registro (transcripción, coste, quién colgó) está disponible en GET /v1/calls/{room}. Las llamadas sin respuesta terminan con ended_by en no_answer.

Llamadas entrantes

Las llamadas entrantes a tus números las atiende el agente asignado a cada número. Los números llegan a través de DIDWW, Twilio o tu propio SIP trunk (Asterisk y otros), y el equipo de la plataforma los añade a tu organización. Tú eliges qué agente contesta:

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

Versiones y publicación segura

Al editar un agente cambias su borrador. Las llamadas a tus números, las llamadas web y las salientes usan la versión publicada, así que puedes trabajar en el borrador sin afectar a las llamadas reales.

  1. Edita el agente con PUT /v1/agents/{id}. Esto solo cambia el borrador; draft_changed pasa a true.
  2. Prueba el borrador: inicia una llamada web con version: "draft".
  3. Publica con POST /v1/agents/{id}/publish y una nota opcional. El borrador se convierte en una nueva versión numerada y pasa a estar activo.
  4. Si algo va mal, POST /v1/agents/{id}/rollback con un número de versión anterior la vuelve a publicar como una versión nueva.
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 y POST /v1/calls aceptan version: "live" (por defecto), "draft" o un número de versión. Un agente que nunca se ha publicado usa su borrador.

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

Prueba antes de publicar

El laboratorio de pruebas ejecuta conversaciones de texto entre un llamante simulado y tu agente, para que puedas comprobar un cambio antes de que lo oigan llamantes reales.

  1. Escribe un escenario: quién llama (persona), qué quiere (goal) y hasta 20 turnos.
  2. Añade las reglas que debe cumplir una buena llamada (expect), por ejemplo "pide el nombre del llamante antes de reservar".
  3. Indica resultados de ejemplo para las herramientas (mocks); no se llama a ninguna herramienta ni webhook real.
  4. Ejecuta los escenarios con el borrador. Un modelo independiente comprueba cada regla y explica por qué se cumplió o no.
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"
}

Las conversaciones se ejecutan en modo texto, así que prueban las instrucciones y el uso de herramientas, no la voz ni la latencia. Las simulaciones se ejecutan desde el panel; todavía no hay un endpoint /v1 para ellas.

Herramientas

Las herramientas permiten que el agente actúe durante una llamada. La misma definición funciona en los cuatro motores; en GPT-Live las ejecuta el modelo en segundo plano. Las herramientas se activan por agente en el campo tools.

Herramientas integradas

NombreQué hace
end_callEl agente se despide y cuelga 0,75 s después de terminar de hablar. Si el llamante habla mientras tanto, no cuelga. El registro de llamada muestra ended_by: "agent".
take_messageRecoge name, phone, message y callback_requested y los guarda en outcomes[] del registro de llamada. No pide confirmación.
search_knowledgeSe añade automáticamente cuando el agente tiene documentos en knowledge. Busca en ellos y anota las fuentes en el registro de llamada.
check_availabilityLectura. Devuelve los huecos libres entre date_from y date_to, dentro del horario laboral del agente. Se añade cuando calendar está configurado.
book_appointmentEscritura, con paso de confirmación. Reserva start a nombre de name con phone, y una note opcional. La reserva se guarda en outcomes[].
cancel_appointmentEscritura, con paso de confirmación. Cancela una reserva por su booking_ref.

Herramientas webhook

Una herramienta webhook es un endpoint HTTPS de tu sistema. Defínela una sola vez para la organización en el panel, en Herramientas, o con POST /v1/tools, y después añade su id a tools.webhooks de un agente (hasta 16 por agente).

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: letras minúsculas, dígitos y guiones bajos, de 3 a 41 caracteres; debe empezar por una letra y ser único en la organización. end_call y take_message están reservados.
  • description le indica al modelo cuándo usar la herramienta. Escríbela como si dieras una instrucción a un compañero.
  • parameters es un objeto JSON Schema con hasta 20 campos de tipo string, number, integer o boolean (se admite enum).
  • kind es read o write; timeout_s va de 2 a 15 segundos.

La solicitud que enviamos

Cuando el modelo llama a la herramienta, la API envía un POST a tu URL. El cuerpo incluye el nombre de la herramienta, los argumentos que eligió el modelo y algo de contexto sobre la llamada:

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

Cada solicitud va firmada con el secreto de la herramienta. X-VT-Timestamp es la hora unix; X-VT-Signature es sha256= seguido del HMAC-SHA256 en hexadecimal de timestamp + "." + body. Compruébala con el cuerpo sin procesar antes de parsearlo y rechaza las marcas de tiempo de hace más de unos minutos.

Verificar la firma

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;

// La firma cubre los bytes exactos: lee el cuerpo sin procesar.
app.post("/hooks/order", express.raw({ type: "*/*" }), (req, res) => {
  const ts = req.get("X-VT-Timestamp") ?? "";
  const sig = req.get("X-VT-Signature") ?? "";

  // Rechaza las solicitudes antiguas (protección contra repetición).
  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"));
  // Responde breve: al modelo solo le llegan los primeros 4 KB.
  res.json({ order_id: args.order_id, status: "shipped", eta: "2026-11-07" });
});

app.listen(3000);

Tu respuesta

Responde con un estado 2xx y JSON o texto plano. Al modelo solo le llegan los primeros 4 KB, así que devuelve solo lo que el agente necesita decir. Un estado distinto de 2xx o un tiempo de espera agotado le indica al modelo que la herramienta falló, y el agente lo gestiona en la conversación.

Herramientas de lectura y escritura

  • read: herramientas que se ejecutan de inmediato, para consultas como el estado de un pedido o el horario de apertura.
  • write: herramientas que pasan por un paso de confirmación. La primera llamada a la herramienta no se ejecuta; el agente repite los detalles al llamante y le pide un sí o un no. Solo se ejecuta si en su siguiente turno el llamante da un sí claro en el idioma de la llamada.
  • La misma llamada a la herramienta (nombre y argumentos) se ejecuta como máximo una vez por conversación.

Seguridad

  • Las URL deben usar https://. Se rechazan las direcciones locales y de redes privadas.
  • El secreto se guarda cifrado y la API nunca lo devuelve (has_secret indica si hay uno configurado). El agente nunca lo ve; la solicitud la hace la API.
  • Los argumentos y las respuestas no se guardan. El registro de llamada conserva tools[] con nombre, tipo, resultado y duración, y una pista TOOL en la línea de tiempo.

Usa POST /v1/tools/{id}/test para enviar una solicitud real con argumentos de ejemplo y ver el estado, el tiempo de respuesta y el cuerpo de la respuesta.

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

Base de conocimiento

La base de conocimiento guarda los documentos de tu organización. Cuando un agente tiene documentos seleccionados, recibe la herramienta search_knowledge y responde con lo que encuentra. Tiene instrucciones de no inventarse lo que no esté en los documentos.

Añadir un documento

Envía un archivo como multipart (file), texto plano como JSON (title, text) o una sola página web como JSON (url). El procesamiento se hace en segundo plano: el documento empieza en processing y pasa a ready o failed.

curl
curl -X POST "https://hubvocal.com/v1/knowledge" \
  -H "Authorization: Bearer $VT_API_KEY" \
  -F "file=@price-list-2026.pdf"
  • Archivos: pdf, docx, txt, md o html, de hasta 10 MB cada uno.
  • Hasta 200 documentos y 200 MB en total por organización.
  • URL: una sola página, solo https; se rechazan las direcciones locales y privadas.

Asignarlos a un agente

Indica los ids de los documentos en knowledge.doc_ids del agente (hasta 50). Prueba algunas búsquedas con POST /v1/knowledge/search antes de pasar a producción.

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

En el registro de llamada, cada búsqueda aparece en tools[] como search_knowledge, con sus sources (documento, título y fragmento).

Calendario y citas

Con un calendario conectado, el agente puede consultar la disponibilidad y reservar o cancelar citas durante la llamada.

Los calendarios se conectan desde el panel, en Integraciones (Google Calendar). No hay ningún endpoint /v1 para las integraciones; en la API solo indicas qué integración usa un agente.

Configuración del agente

Configura el objeto calendar del agente: qué integración y qué calendario, duración de la cita (15–240 min), margen entre citas (0–60 min), zona horaria, horario laboral, antelación mínima (0–72 h) y con cuánto tiempo se puede reservar como máximo (1–90 días).

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

El agente recibe entonces tres herramientas integradas: check_availability (lectura), book_appointment y cancel_appointment (ambas de escritura). Las de escritura pasan por el paso de confirmación: el agente repite la hora, el nombre y el teléfono, y solo reserva tras un sí claro.

Cada reserva se guarda en el registro de llamada, en outcomes[], como {kind: "appointment", start, name, phone, ref}.

MCP

HubVocal admite el Model Context Protocol de dos formas: tus agentes pueden usar herramientas de tus propios servidores MCP, y los clientes MCP pueden gestionar la plataforma.

Conecta un servidor MCP a tus agentes

Añade tu servidor con POST /v1/mcp-servers o desde el panel. Obtenemos su lista de herramientas (tools/list); las nuevas llegan desactivadas. Activa las que quieras, marca cada una como read o write e inclúyelas en tools.mcp del agente.

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_…"
}'
  • La conexión la hace la API, no el agente, así que el agente nunca ve el secreto de tu servidor.
  • Las URL deben ser https; se rechazan las direcciones locales y privadas, también en el momento de conectar.
  • Las herramientas write pasan por el paso de confirmación: el agente repite los datos y ejecuta la herramienta solo tras un sí claro, como máximo una vez por llamada.
  • Cada invocación expira a los 10 segundos; al modelo llegan como máximo 4 KB del resultado.
  • Hasta 10 servidores por organización y 50 herramientas por servidor. Las herramientas MCP cuentan para el límite de 16 herramientas del agente.

En la conversación y en el registro de llamada, una herramienta MCP se llama <server>.<tool>, por ejemplo crm.find_customer.

Gestiona la plataforma desde un cliente MCP

HubVocal también es un servidor MCP en https://hubvocal.com/mcp (streamable HTTP). Autentícate con una clave de API de la organización como token Bearer. El límite de solicitudes se comparte con /v1.

Conecta un cliente

Sustituye vt_live_… por tu clave. Usa una clave que tenga solo los ámbitos que el cliente necesita.

Claude Code
claude mcp add --transport http hubvocal https://hubvocal.com/mcp \
  --header "Authorization: Bearer vt_live_…"

En la app de ChatGPT, los conectores MCP personalizados se añaden en el modo desarrollador; que puedas usar un encabezado con la clave de API depende de tu plan y de la configuración del conector. Con la API de OpenAI puedes pasar la clave directamente, como se muestra abajo.

Herramientas

HerramientaÁmbitoQué hace
list_agentsreadLista tus agentes.
get_agentreadLee un agente.
list_callsreadLista llamadas, filtradas por since, outcome o agent_id (hasta 50).
get_callreadLee una llamada con su transcripción y su resumen.
list_numbersreadLista tus números de teléfono.
list_knowledgereadLista los documentos de la base de conocimiento.
search_knowledgereadBusca en la base de conocimiento.
get_usagereadCoste y minutos de este mes.
create_agentwriteCrea un agente.
update_agentwriteModifica el borrador de un agente.
publish_agentwritePublica el borrador de un agente.
add_knowledge_textwriteAñade un documento de texto a la base de conocimiento.
start_outbound_callcallsRealiza una llamada saliente (agent_id, to). Se aplican las reglas y los límites habituales de las llamadas salientes.

Recursos y prompts

  • agent://{id}: la configuración de un agente.
  • call://{id}: la transcripción de una llamada en texto.
  • Prompt daily_call_report: un resumen de las llamadas del día.

Ámbitos y registro de auditoría

Cada clave de API tiene ámbitos, que eliges al crearla en el panel:

  • read: herramientas y endpoints de solo lectura.
  • write: además, crear y modificar agentes y documentos.
  • calls: además, realizar llamadas salientes.

Las claves creadas antes de que existieran los ámbitos tienen los tres. Cada llamada a una herramienta MCP queda en el registro de auditoría con la clave, la herramienta, la hora y el resultado.

Widget web

El widget añade un botón de llamada a tu sitio web. Los visitantes hablan con tu agente en el navegador; no necesitas código de servidor por tu parte.

Crea una clave pública

En el panel, abre el agente y crea una clave pública (pk_…). Una clave pública pertenece a un solo agente y puede ir en tu página sin riesgo, porque solo permite llamadas web con ese agente y solo desde los dominios que autorices.

  • Dominios permitidos (allowed_origins): se rechazan las llamadas desde otros sitios.
  • Un límite de tiempo por llamada.
  • Como máximo 10 llamadas por hora desde la misma dirección IP.

Añade el script

Pégalo antes de la etiqueta de cierre </body>:

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

El color, la posición y el texto del botón (en el idioma del visitante) se toman de los ajustes widget del agente: color, position, label.

JSON
{
  "widget": {
    "color": "#2563eb",
    "position": "bottom-right",
    "label": "Talk to us"
  }
}

El widget llama a POST /api/public/web-calls con la clave pública; nunca ve tu clave de API. Las claves públicas se gestionan en el panel.

Campos estructurados hacia tu CRM

Los campos estructurados convierten una conversación en datos que tus sistemas pueden usar, por ejemplo la fecha de la cita, el producto por el que alguien preguntó o si quiere que le devuelvan la llamada.

Define los campos

Añade hasta 24 campos en post_call.fields del agente. Cada uno tiene un name (letras minúsculas, dígitos y guiones bajos), un type (string, number, boolean, enum o date), una description que indica al modelo qué buscar y, opcionalmente, valores enum y 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?"
      }
    ]
  }
}

Tras la llamada, el mismo modelo que redacta el resumen rellena los campos. Los valores que no se mencionaron son null. Los recibes como extracted en GET /v1/calls/{room} y en la lista de llamadas, y en el cuerpo del webhook call.ended.

JSON
{
  "extracted": {
    "appointment_date": "2026-11-12",
    "service": "cleaning",
    "callback": true
  }
}

La exportación CSV incluye cada campo como una columna.

Webhooks de llamada

Un webhook envía un evento call.ended a tu sistema cuando una llamada ha terminado y su registro, resumen incluido, está completo. Úsalo para crear un ticket, actualizar un contacto en tu CRM o enviar un seguimiento.

Si las puntuaciones de calidad de llamada están activadas, también puedes suscribirte a call.quality_low. Se envía una vez por llamada cuando la puntuación cae por debajo del umbral de tu organización.

Añade hasta 5 webhooks por organización desde el panel o con POST /v1/webhooks. Si no envías un secret, se genera uno y se devuelve solo en esa respuesta. Las URL deben ser https; se rechazan las direcciones locales y privadas.

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

El evento

data es el mismo objeto de llamada que devuelve GET /v1/calls/{room}, con la transcripción y el resumen:

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
      }
    ]
  }
}

Las solicitudes se firman igual que las solicitudes de herramientas: X-VT-Timestamp y X-VT-Signature. Se añaden dos encabezados más: X-VT-Event (el tipo de evento) y X-VT-Delivery (un id único para esta entrega).

Reintentos

Cualquier respuesta 2xx en menos de 10 segundos cuenta como entregada. Si no, reintentamos:

  1. al cabo de 1 minuto
  2. al cabo de 5 minutos
  3. al cabo de 30 minutos
  4. al cabo de 2 horas

Tras 5 intentos en total, la entrega se marca como failed. Puedes ver cada intento con GET /v1/webhooks/{id}/deliveries y volver a poner en cola una entrega fallida con POST /v1/webhooks/deliveries/{id}/retry.

Gestiona los duplicados

Un reintento puede llegar aunque tu respuesta anterior se haya perdido por el camino, así que el mismo evento puede llegar más de una vez. Guarda el id de X-VT-Delivery y descarta los que ya hayas procesado. Responde rápido y deja el trabajo lento para segundo plano.

Node (Express)
// Primero verifica la firma (el mismo código que para las herramientas).
app.post("/hooks/voice", express.raw({ type: "*/*" }), async (req, res) => {
  if (!verify(req)) return res.sendStatus(401);

  // ¿Ya procesaste esta entrega? Confírmala y no sigas.
  const delivery = req.get("X-VT-Delivery");
  if (await db.deliveries.exists(delivery)) return res.sendStatus(200);
  await db.deliveries.insert(delivery);

  // Responde rápido; haz el trabajo lento en segundo plano.
  res.sendStatus(200);
  const event = JSON.parse(req.body.toString("utf8"));
  queue.add("call-ended", event.data);
});

POST /v1/webhooks/{id}/test envía un evento call.ended de ejemplo y devuelve el estado, el tiempo de respuesta y el cuerpo de la respuesta.

Idiomas de conversación compatibles

En qué idiomas puede conversar cada motor. El idioma de un agente se define con el campo lang, usando los códigos de abajo.

  • Compatible
  • Experimental El motor no incluye oficialmente este idioma. Funciona si se le indica en las instrucciones, pero no hemos medido la calidad.
Idioma lang GPT-Live 1Gemini LiveOpenAI RealtimeClásico
Türkçetr tr Experimental Compatible Experimental Compatible
Englishen en Compatible Compatible Experimental Compatible
Deutschde de Compatible Compatible Experimental Compatible
Françaisfr fr Compatible Compatible Experimental Compatible
Españoles es Experimental Compatible Experimental Compatible
Italianoit it Experimental Compatible Experimental Compatible
Portuguêspt pt Compatible Compatible Experimental Compatible
Nederlandsnl nl Experimental Compatible Experimental Compatible
Русскийru ru Experimental Compatible Experimental Compatible
Українськаuk uk Experimental Compatible Experimental Compatible
Polskipl pl Experimental Compatible Experimental Compatible
Românăro ro Experimental Compatible Experimental Compatible
Ελληνικάel el Experimental Compatible Experimental Compatible
Българскиbg bg Experimental Compatible Experimental Compatible
Češtinacs cs Experimental Compatible Experimental Compatible
Slovenčinask sk Experimental Compatible Experimental Compatible
Magyarhu hu Experimental Compatible Experimental Compatible
Hrvatskihr hr Experimental Compatible Experimental Compatible
Српскиsr sr Experimental Compatible Experimental Compatible
Bosanskibs bs Experimental Compatible Experimental Compatible
Slovenščinasl sl Experimental Compatible Experimental Compatible
Македонскиmk mk Experimental Compatible Experimental Compatible
Svenskasv sv Experimental Compatible Experimental Compatible
Danskda da Experimental Compatible Experimental Compatible
Norskno no Experimental Compatible Experimental Compatible
Suomifi fi Experimental Compatible Experimental Compatible
Eestiet et Experimental Compatible Experimental Compatible
Latviešulv lv Experimental Compatible Experimental Compatible
Lietuviųlt lt Experimental Compatible Experimental Compatible
Catalàca ca Experimental Compatible Experimental Compatible

Prueba los idiomas experimentales en el navegador antes de llevarlos a una línea telefónica.

Límites y errores

El límite es de 120 solicitudes por minuto por clave. Si lo superas, recibes 429 con un encabezado Retry-After en segundos. Los errores llegan con un código de estado HTTP y siempre con el mismo cuerpo; detail es un mensaje legible.

402
{
  "detail": "Monthly spending limit reached"
}
CódigoSignificadoCuándo
400Solicitud incorrectaNo se pudo leer el cuerpo, falta un campo obligatorio o un valor no es válido (por ejemplo, un número que no está en E.164 o borrar el último agente).
401No autorizadoFalta el encabezado Authorization, la clave no empieza por vt_live_ o ha sido revocada.
402Límite alcanzadoLa organización alcanzó su límite de gasto mensual; no se pueden iniciar llamadas nuevas.
403SuspendidaLa organización está suspendida; no se pueden iniciar llamadas nuevas.
404No encontradoNo existe ese agente, llamada, número o endpoint.
409ConflictoSe envió POST /v1/agents con un id que ya existe.
429Demasiadas solicitudesMás de 120 solicitudes por minuto con una misma clave. Espera los segundos que indica Retry-After.
502Error del proveedorEl proveedor de telefonía rechazó la llamada saliente o no respondió.
500Error del servidorOcurrió algo inesperado. Vuelve a intentarlo; si persiste, escríbenos.

Reintenta solo las respuestas 429 y 5xx. Un 4xx indica que algo falla en la propia solicitud; enviarla de nuevo no cambiará el resultado.

Reintentos con 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;
}