Prueba gratis

Documentación de la API

La API de Voza usa HTTPS, JSON y los verbos HTTP de siempre. Los cuerpos de mensajes, llamadas y webhooks siguen el formato de la API oficial de WhatsApp (Meta), así que el mismo código sirve para los dos tipos de número de Voza.

Dirección base:

https://www.vozaapi.com/v1
Número por código QRNúmero oficial (Meta)
Cómo se conectaEscaneando un código QR con el celular, como en WhatsApp WebCon las credenciales de tu app de Meta (WhatsApp Cloud API)
Cuenta de WhatsAppWhatsApp o WhatsApp Business común; el celular sigue funcionandoWhatsApp Business Platform, en tu cuenta de Meta
MensajesTexto y archivos multimediaTexto, archivos multimedia, plantillas, mensajes interactivos, stickers, ubicación, contactos y reacciones
Ventana de 24 horasNo hayFuera de ella, solo plantillas aprobadas por Meta
LlamadasSí, varias al mismo tiempo en el mismo númeroSí, cuando Meta habilita las llamadas para el número
Cobro de las conversacionesNinguno además del planMeta cobra en tu cuenta de Meta
RiesgoWhatsApp puede bloquear números que envían spamSigue las políticas y la calidad de Meta

Inicio rápido

  1. Crea una cuenta y, en Panel › Claves de API, una clave.
  2. Crea un número:
curl https://www.vozaapi.com/v1/numbers \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"name": "Tienda Centro", "webhook_url": "https://tu-sistema.com/webhooks/whatsapp"}'
  1. Abre el qr_url de la respuesta y escanea el código QR con WhatsApp en el celular (Dispositivos vinculados › Vincular un dispositivo). O conecta un número oficial (consulta Números oficiales).
  2. Envía el primer mensaje:
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/messages \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"to": "5511987654321", "type": "text", "text": {"body": "¡Hola! 👋"}}'
  1. Las respuestas del contacto llegan a tu webhook, en el formato de Meta. Listo: el resto de esta página son detalles.

Autenticación

Crea una clave en Panel › Claves de API y envíala en todas las solicitudes, en el encabezado Authorization:

Authorization: Bearer vz_SUA_CHAVE

Errores

Todos los errores tienen el mismo cuerpo. El código es estable y sirve para que tu sistema decida qué hacer; el mensaje es para personas y puede cambiar.

El mensaje llega en el idioma pedido en el encabezado Accept-Language o en el parámetro ?lang=: pt-BR, en o es (otro idioma recibe inglés). Sin pedido, llega en el idioma de quien creó la cuenta.

{
  "error": {
    "code": "invalid_request",
    "message": "El campo to es obligatorio.",
    "details": { "to": ["El campo to es obligatorio."] }
  }
}

Cuando la falla viene de Meta, en un número oficial, el error trae también meta: el error de Meta tal como lo dio, con code (131047 fuera de la ventana de 24 horas, 190 token inválido, 1380xx en las llamadas…), subcode, type, details y fbtrace_id. Cuando viene del motor de los números por código QR, meta.reason dice por qué: not_on_whatsapp, lines_busy, lines_starting, restarting, not_linked, not_connected, timeout…

{
  "error": {
    "code": "whatsapp_refused",
    "message": "Meta no aceptó la solicitud: Message failed to send because more than 24 hours have passed.",
    "meta": {
      "code": 131047,
      "subcode": null,
      "type": "OAuthException",
      "details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
      "fbtrace_id": "AbCdEf123"
    }
  }
}
EstadoCódigoCuándo ocurre
401invalid_api_keyLa clave falta, es inválida o fue revocada.
401invalid_webphone_tokenEl token del teléfono web falta, es inválido o venció.
403trial_expiredLa prueba gratis terminó: suscríbete a un plan en el panel.
403subscription_inactiveLa suscripción terminó (cancelada o sin pago): suscríbete de nuevo en el panel.
403account_suspendedLa cuenta está suspendida.
403number_limit_reachedLa cuenta ya tiene todos los números del plan.
404not_foundLa dirección o el recurso no existe (o es de otra cuenta).
404call_not_foundLa llamada no existe o ya terminó.
405method_not_allowedLa dirección existe, pero no con este método HTTP. El encabezado Allow indica cuáles valen.
409number_not_connectedEl número por código QR no está conectado: escanea el código QR antes de enviar o llamar.
409meta_token_invalidMeta rechazó el token del número oficial (venció, fue revocado o no tiene los permisos). Envía uno nuevo a PUT /v1/numbers/{id}/meta.
409not_a_qr_numberLa solicitud solo es válida para números por código QR.
409not_an_official_numberLa solicitud solo es válida para números oficiales.
409send_in_progressOtro envío con la misma Idempotency-Key todavía está en curso.
409action_in_progressOtra acción de llamada con la misma Idempotency-Key todavía está en curso.
409call_answered_elsewhereOtro teléfono web, el agente de voz o su sistema ya atendió esta llamada.
409call_not_readyLa llamada todavía se está preparando; inténtalo de nuevo en unos instantes.
413media_too_largeEl archivo multimedia recibido por un número por código QR superaba los 64 MB y no se guardó.
422invalid_requestLos datos enviados son inválidos. El campo details lista los errores de cada campo.
422unsupported_typeEl tipo de mensaje solo existe en los números oficiales (plantilla, interactivo…).
422meta_refusedMeta rechazó las credenciales al conectar un número oficial; el mensaje indica el motivo.
422whatsapp_refusedMeta rechazó el mensaje de un número oficial (plantilla inexistente, parámetro inválido…).
422not_on_whatsappEl número llamado no tiene WhatsApp.
422call_failedMeta no aceptó la acción de llamada de un número oficial (sin permiso del contacto, por ejemplo).
429rate_limitedEl límite de solicitudes de la cuenta (espera los segundos de Retry-After) o el límite de envíos de Meta en un número oficial (envía más despacio).
429channel_limit_reachedLa cuenta ya está en el límite de llamadas simultáneas del plan.
429lines_busyTodas las líneas del número están en una llamada.
4xxhttp_errorOtro error de HTTP, como un cuerpo demasiado grande (413). El estado indica cuál.
500server_errorError interno. Inténtalo de nuevo; si el error continúa, contáctanos.
502whatsapp_refusedWhatsApp no aceptó el mensaje de un número por código QR (el destino no tiene WhatsApp, por ejemplo).
502call_failedWhatsApp no aceptó la acción de llamada de un número por código QR.
502media_unavailableWhatsApp no entregó un archivo multimedia recibido por un número por código QR.
503number_reconnectingEl número por código QR se está reconectando a WhatsApp. Inténtalo de nuevo en unos instantes.
503engine_unavailableEl servicio que mantiene los números por código QR no está disponible. Inténtalo de nuevo en unos instantes.
503meta_unavailableLa solicitud no llegó a Meta (sin conexión) o Meta respondió que no está disponible: no se hizo nada. Inténtalo de nuevo en unos instantes.
503lines_startingLas líneas de llamada del número por código QR se están iniciando. Inténtalo de nuevo en unos segundos.
504send_timeoutEl envío tardó demasiado. Repítelo con la misma Idempotency-Key.
504meta_timeoutMeta no respondió a tiempo después de recibir la solicitud (un envío o una acción de llamada): puede haberse hecho o no. Repítela con la misma Idempotency-Key.
504call_timeoutEl servicio que mantiene los números por código QR tardó demasiado en responder a una acción de llamada.

Un 503 suele pasar en instantes: inténtalo de nuevo, esperando un poco más cada vez. Para saber si Voza está caída, consulta Estado de Voza.

Límites

Idempotencia

Al enviar un mensaje, manda el encabezado Idempotency-Key (hasta 128 letras, números y _ . : -). Repetir el envío con la misma clave devuelve la respuesta del primero en lugar de enviar de nuevo. Úsalo siempre que tu sistema vaya a repetir un envío que falló o se quedó sin respuesta.

Idempotency-Key: pedido-4521-aviso-entrega

Voza guarda la primera respuesta durante 24 horas, en los dos tipos de número. Un envío repetido mientras el primero sigue en curso lo espera hasta 30 segundos; después, la respuesta es 409 send_in_progress, y basta con intentarlo de nuevo con la misma clave.

Las acciones de llamada (POST /v1/numbers/{id}/calls) aceptan el mismo encabezado: repetir con la misma clave devuelve la primera respuesta en lugar de repetir la acción (un connect no llama dos veces). Una repetida mientras la primera está en curso recibe 409 action_in_progress.

En un número oficial, 503 meta_unavailable significa que la solicitud no llegó a Meta, y 504 meta_timeout, que llegó, pero Meta no respondió a tiempo: el mensaje puede haberse enviado (o la acción hecho). En los dos casos, repítela con la misma Idempotency-Key: un error no se guarda, así que la repetición va a Meta; Meta no tiene idempotencia propia, y después de un meta_timeout el mensaje puede llegar dos veces al contacto.

Números por código QR

Un número por código QR es un WhatsApp o WhatsApp Business común conectado como dispositivo, como en WhatsApp Web. El celular sigue funcionando normalmente.

El objeto número

{
  "id": "01jbk3m8x2c9q4r7t6v5w0y1z3",
  "name": "Tienda Centro",
  "external_ref": "canal-981",
  "type": "qr",
  "status": "connected",
  "status_reason": null,
  "phone": "5511987654321",
  "qr_url": "https://www.vozaapi.com/qr/…",
  "webhook_url": "https://tu-sistema.com/webhooks/whatsapp",
  "agent_mode": "off",
  "connected_at": "2026-10-22T15:04:05Z",
  "created_at": "2026-10-22T15:01:10Z"
}
CampoDescripción
external_refTu referencia para el número (hasta 100 caracteres), como el id del canal en tu sistema. Vuelve en los webhooks, en metadata.external_ref, para que encuentres el canal sin guardar el id de Voza.
typeqr (código QR) o meta (oficial).
status_reasonPor qué el número está en su status actual (ver Status); vacío cuando no hay nada que explicar.
phoneLos dígitos del número, con el código de país, una vez conectado.
qr_urlUna página pública con el código QR, para enviarla a quien tiene el celular. Sigue la conexión sola.
agent_modeCuándo atiende el agente de voz del número: off, always o fallback (vea Agentes de voz).
connected_atCuándo se conectó el número, en UTC (ISO 8601).

Estado

EstadoSignificado
startingLa conexión está empezando; el código QR aparece a continuación.
qrEsperando que se escanee el código QR.
connectedConectado: el número envía y recibe mensajes y llamadas.
failedRechazado: por WhatsApp (un número bloqueado, por ejemplo; usa relink) o, en un número oficial, por Meta (consulta meta.error).

Si alguien desconecta Voza en el celular (Dispositivos vinculados), el número vuelve a starting y luego a qr, con un nuevo código QR. Cada cambio llega al webhook como number.status.

status_reason dice por qué el número está en su status actual, y un cambio solo del motivo también llega al webhook:

status_reasonSignificado
logged_outEl celular desconectó el dispositivo (o el número se volvió a vincular): hay que leer el código QR de nuevo.
restartingEl motor está reconectando el número, que sigue vinculado al celular: espera, sin hacer nada.
failedWhatsApp rechazó el dispositivo.
meta_refusedMeta rechazó el token o el número oficial (ver meta.error).

En los demás casos (qr, connected, un número nuevo) el motivo viene vacío.

Crear un número

POST /v1/numbers
{
  "name": "Tienda Centro",
  "external_ref": "canal-981",
  "webhook": { "url": "https://tu-sistema.com/webhooks/whatsapp" }
}

Responde 201 con el número en starting. external_ref y webhook son opcionales (también se acepta webhook_url, en lugar de webhook.url). Con un webhook en la solicitud, la respuesta trae también el secreto que firma los eventos, para verificar ya el primero:

{
  "id": "01jbk3m8x2c9q4r7t6v5w0y1z3",
  "status": "starting",
  …
  "webhook": { "url": "https://tu-sistema.com/webhooks/whatsapp", "secret": "whsec_…" }
}

La dirección del webhook puede nombrar el número: {id} se convierte en el id del número en Voza y {external_ref}, en tu referencia (codificada para URL; la solicitud debe traer external_ref). El webhook queda definido antes de que el número se conecte, así que no se pierde ningún evento, sin un PUT después.

{ "name": "Tienda Centro", "external_ref": "canal-981", "webhook": { "url": "https://tu-sistema.com/webhooks/voza/{external_ref}?numero={id}" } }

Por encima del límite del plan, responde 403 con number_limit_reached.

Mostrar el código QR

GET /v1/numbers/{id}/qr
{
  "status": "qr",
  "qr": "data:image/png;base64,iVBORw0KGgo…",
  "qr_url": "https://www.vozaapi.com/qr/…"
}

qr es una imagen PNG lista para un <img src>. El código QR cambia cada 20 segundos: consulta de nuevo cada 3 a 5 segundos hasta que el estado sea connected (entonces qr viene vacío). O envía el qr_url a quien tiene el celular.

Listar, consultar y eliminar

GET /v1/numbers

Todos los números de la cuenta, en {"data": [...]}. Con ?external_ref=canal-981, solo los que tienen esa referencia.

GET /v1/numbers/{id}

El número, con el estado leído en el momento.

PATCH /v1/numbers/{id}

Cambia el name o la external_ref del número ("external_ref": null la elimina) y responde con el número.

DELETE /v1/numbers/{id}

Desconecta el celular y elimina el número. Responde 204.

Conectar otro celular

POST /v1/numbers/{id}/relink

Desconecta el celular actual y empieza de nuevo, con un nuevo código QR. Sirve para cambiar de celular o para recuperar un número rechazado.

Números oficiales

Un número oficial es un número de la WhatsApp Cloud API, en tu cuenta de Meta. Voza usa las credenciales de tu app: los mensajes y las llamadas pasan por la API de Meta, y llegan a tu webhook en el mismo formato que los números por código QR.

Antes de conectar, en Meta

  1. Ten una app de Meta con el producto WhatsApp y un número de teléfono en la cuenta de WhatsApp Business (WABA).
  2. En WhatsApp › Configuración, suscríbete a los campos messages y calls del webhook de la app (con cualquier dirección de webhook: Voza apunta hacia sí misma solo este número, con una sustitución por número, y los demás eventos de tu app siguen donde están).
  3. Crea un token permanente de un usuario del sistema, con los permisos whatsapp_business_messaging y whatsapp_business_management.
  4. Anota el ID del número de teléfono y el ID de la cuenta de WhatsApp Business (WhatsApp › Configuración de la API), y la clave secreta de la app (Configuración de la app › Básica).

Conectar

POST /v1/numbers
{
  "name": "Atención oficial",
  "type": "meta",
  "webhook_url": "https://tu-sistema.com/webhooks/whatsapp",
  "meta": {
    "phone_number_id": "109876543210987",
    "waba_id": "102030405060708",
    "access_token": "EAAG…",
    "app_secret": "0123456789abcdef0123456789abcdef"
  },
  "calling": { "call_icon_visibility": "DISABLE_ALL" }
}

Voza verifica el número y el token en Meta, suscribe tu app a la cuenta, apunta los webhooks messages y calls del número hacia Voza y activa las llamadas. Si todo sale bien, responde 201 con el número ya connected:

{
  "id": "01jbk4…",
  "type": "meta",
  "status": "connected",
  "phone": "5511912345678",
  "qr_url": null,
  "meta": {
    "phone_number_id": "109876543210987",
    "waba_id": "102030405060708",
    "display_phone_number": "+55 11 91234-5678",
    "verified_name": "Tu Empresa",
    "quality_rating": "GREEN",
    "calling": "enabled",
    "error": null
  }
}

Cambiar el token

PUT /v1/numbers/{id}/meta
{ "access_token": "EAAG…nuevo", "app_secret": "(solo si cambió)" }

Guarda las nuevas credenciales y conecta el número de nuevo. POST /v1/numbers/{id}/relink solo verifica de nuevo, con las credenciales actuales. DELETE devuelve los webhooks del número a tu app y lo saca de Voza (el número sigue en tu cuenta de Meta).

Ni el cambio de token ni el relink tocan la configuración de llamadas: Voza solo la define cuando se crea el número.

Configuración de llamadas

GET /v1/numbers/{id}/calling
PUT /v1/numbers/{id}/calling

El objeto calling de la configuración del número en Meta, leído y cambiado como en Meta. En el PUT, solo cambian los campos enviados, y la respuesta es {"success": true}.

{
  "calling": {
    "status": "ENABLED",
    "call_icon_visibility": "DEFAULT",
    "callback_permission_status": "ENABLED",
    "call_hours": {
      "status": "ENABLED",
      "timezone_id": "America/Sao_Paulo",
      "weekly_operating_hours": [{ "day_of_week": "MONDAY", "open_time": "0900", "close_time": "1800" }]
    }
  }
}
CampoDescripción
statusENABLED o DISABLED: las llamadas del número.
call_icon_visibilityDEFAULT o DISABLE_ALL: el botón de llamada en la conversación.
callback_permission_statusENABLED o DISABLED: pedir al contacto que llamó permiso para devolverle la llamada.
call_hours, call_iconsEl horario de atención y los países que ven el botón, como en Meta, que los verifica.

sip queda fuera: las llamadas pasan por Voza. En un número por código QR, la respuesta es 409 not_an_official_number.

Enviar mensajes

El cuerpo es el mismo de la API oficial de WhatsApp. El número debe estar conectado.

POST /v1/numbers/{id}/messages
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/messages \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-4521" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "5511987654321",
    "type": "text",
    "text": { "body": "¡Tu pedido salió para entrega!" }
  }'
{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "5511987654321", "wa_id": "5511987654321" }],
  "messages": [{ "id": "3EB0C431D5A1B2C3" }]
}

Tipos de mensaje

TipoCuerpoNúmeros
text{"text": {"body": "…"}} (hasta 4096 caracteres)todos
image, video{"image": {"link": "https://…", "caption": "…"}}todos
audio{"audio": {"link": "https://…"}}; con "voice": true, nota de voz (usa OGG/Opus)todos
document{"document": {"link": "https://…", "filename": "factura.pdf"}}todos
templateUna plantilla aprobada: {"template": {"name": "…", "language": {"code": "pt_BR"}, "components": […]}}oficiales
interactiveBotones, listas, enlaces y la solicitud de permiso para llamar, como en Metaoficiales
sticker, location, contacts, reactionComo en la API de Metaoficiales

La ventana de 24 horas (números oficiales)

En un número oficial, Meta solo entrega mensajes libres a quienes escribieron al número en las últimas 24 horas. Fuera de esa ventana, inicia la conversación con una plantilla aprobada en WhatsApp Manager:

{
  "to": "5511987654321",
  "type": "template",
  "template": {
    "name": "pedido_enviado",
    "language": { "code": "pt_BR" },
    "components": [{ "type": "body", "parameters": [{ "type": "text", "text": "4521" }] }]
  }
}

La API acepta un mensaje libre fuera de la ventana, pero llega al webhook con el estado failed, con el error de Meta.

Plantillas de mensaje

GET /v1/numbers/{id}/templates

Las plantillas de la cuenta de WhatsApp Business de un número oficial, leídas en Meta, una página a la vez, en la respuesta de Meta. Cada plantilla trae id, name, language, category, status y components, o los campos pedidos en fields.

{
  "data": [{
    "id": "1234567890",
    "name": "pedido_enviado",
    "language": "pt_BR",
    "category": "UTILITY",
    "status": "APPROVED",
    "components": [{ "type": "BODY", "text": "Seu pedido {{1}} saiu para entrega." }]
  }],
  "paging": {
    "cursors": { "before": "MAZDZD", "after": "MjQZD" },
    "next": "https://www.vozaapi.com/v1/numbers/NUMBER_ID/templates?after=MjQZD"
  }
}

Botones

{
  "to": "5511987654321",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": { "text": "¿Confirmas la entrega mañana a las 10?" },
    "action": { "buttons": [
      { "type": "reply", "reply": { "id": "confirm", "title": "Sí" } },
      { "type": "reply", "reply": { "id": "reschedule", "title": "Reprogramar" } }
    ] }
  }
}

La elección del contacto llega al webhook como un mensaje del tipo interactive, con el id del botón.

Archivos multimedia

Los mensajes recibidos con imagen, video, audio, documento o sticker traen el id del archivo (en image.id, audio.id…). Descárgalo con:

GET /v1/numbers/{id}/media/{media_id}

Webhooks

Cada número tiene su propio webhook. Voza le envía los mensajes recibidos, los estados de entrega de los mensajes enviados, los eventos de llamada y los cambios de estado del número.

PUT /v1/numbers/{id}/webhook
curl -X PUT https://www.vozaapi.com/v1/numbers/NUMBER_ID/webhook \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://tu-sistema.com/webhooks/whatsapp"}'
{ "url": "https://tu-sistema.com/webhooks/whatsapp", "secret": "whsec_…" }

La dirección debe ser https pública. Envía "url": null para eliminar el webhook y, junto con la url, "rotate_secret": true para cambiar el secreto. GET en la misma dirección muestra la configuración actual.

Mensajes y estados de entrega

Llegan en el mismo sobre de la API oficial, así que el código que lee los webhooks de Meta lee los de Voza:

{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "42",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": { "display_phone_number": "5511987654321", "phone_number_id": "NUMBER_ID", "external_ref": "canal-981" },
        "contacts": [{ "wa_id": "5521999990000", "profile": { "name": "Bruno" } }],
        "messages": [{
          "from": "5521999990000",
          "id": "ABCD1234",
          "timestamp": "1761150000",
          "type": "text",
          "text": { "body": "Hola, ¿cómo estás?" }
        }]
      }
    }]
  }]
}

Un estado de entrega llega en el mismo sobre, en statuses:

"statuses": [{
  "id": "3EB0C431D5A1B2C3",
  "status": "delivered",
  "timestamp": "1761150030",
  "recipient_id": "5521999990000"
}]

Estado del número

{
  "object": "voza",
  "event": "number.status",
  "number": { "id": "NUMBER_ID", "name": "Tienda Centro", "external_ref": "canal-981", "status": "starting", "status_reason": "logged_out", "phone": null },
  "timestamp": 1761150000
}

El evento test, enviado desde el panel, tiene la misma forma. status_reason dice por qué cambió el número (ver Status).

Encabezados y firma

EncabezadoContenido
X-Voza-Signaturesha256= y el HMAC-SHA256 del cuerpo, con el secreto del webhook. El mismo valor va en X-Hub-Signature-256, como en Meta.
X-Voza-DeliveryEl id de la entrega, igual en todos los intentos: úsalo para ignorar repeticiones.
X-Voza-Eventmessages, calls, number.status o test
User-AgentVoza-Webhooks/1.0

Verifica la firma con el cuerpo sin procesar, antes de leer el JSON:

// PHP
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('VOZA_WEBHOOK_SECRET'));
if (! hash_equals($expected, $_SERVER['HTTP_X_VOZA_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

// Node.js (body es el Buffer sin procesar de la solicitud)
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.VOZA_WEBHOOK_SECRET).update(body).digest('hex');
const given = req.headers['x-voza-signature'] || '';
const valid = given.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));

# Python (body son los bytes sin procesar de la solicitud)
expected = 'sha256=' + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, request.headers.get('X-Voza-Signature', ''))

Respuestas y reintentos

Teléfono web

La forma más simple de contestar y hacer llamadas: un SDK en JavaScript, sin dependencias, que corre en el navegador de tus agentes. La clave de API queda en tu servidor; el navegador recibe solo un token de corta duración, de un número. Funciona con los dos tipos de número.

1. En tu servidor, pide un token para el agente:

POST /v1/numbers/{id}/webphone-tokens
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/webphone-tokens \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"label": "Ana", "minutes": 480}'
{ "token": "wp_…", "expires_at": "2026-10-23T01:00:00Z", "sdk_url": "https://www.vozaapi.com/sdk/webphone.js" }

minutes va de 5 a 720 (8 horas por defecto). Usa un token por pestaña del navegador.

2. En la página del agente, el teléfono listo:

<script src="https://www.vozaapi.com/sdk/webphone.js"></script>
<voza-phone token="wp_…" lang="es"></voza-phone>

O tu propia interfaz, con la API del SDK:

const phone = new VozaWebphone({ token: 'wp_…' });
phone.on('incoming', (call) => {
  // call.contact, call.contactName; call.accept() o call.reject()
});
phone.on('call', (call) => console.log(call.direction, call.state));
phone.on('error', (error) => console.warn(error.code, error.message));
phone.on('disconnected', () => { /* el token venció: pide otro */ });
await phone.connect();

const call = await phone.call('5511987654321');
call.mute(true);
call.hangup();

phone.disconnect(); // deja de seguir las llamadas y cuelga las que están en curso
EventoCuándo ocurre
readyEl teléfono está listo. Recibe la sesión, con el número (consulta API del teléfono web).
stateEl teléfono cambió de estado: connecting, ready o closed.
incomingUna llamada recibida empezó a sonar.
callCualquier cambio en una llamada: un nuevo estado o el silencio.
endedUna llamada terminó; call.reason indica por qué.
monitorUna supervisión abierta con phone.monitor() cambió de estado (consulta Supervisar una llamada de la IA).
errorUn error, con error.code: microphone_denied, microphone_unavailable, insecure_context, unsupported_browser, connection_failed, network_error o un código de la API, como not_on_whatsapp.
disconnectedEl token venció o fue rechazado: pide otro y crea un nuevo teléfono.

Supervisar una llamada de la IA

Un supervisor escucha, en el navegador, una llamada que contestó el agente de voz, y puede tomarla y devolverla a la IA. Entrar solo pide un token corto; tomar, devolver y susurrar necesitan la clave de API, así que el SDK llama a funciones de tu página, y estas le piden a tu servidor.

1. En tu servidor, pide un token de supervisión con POST /v1/numbers/{id}/calls/{call_id}/monitor y pasa {token, ice_servers} al navegador. Cada token entra una vez, en hasta 5 minutos.

2. En el navegador, con la API del SDK:

// tu función que llama a tu servidor
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });

const monitor = await phone.monitor(callId, { // o VozaWebphone.monitor(), sin teléfono web
  token: 'mon_…',
  iceServers, // los ice_servers del token
  takeover: ({ callId, monitorId }) => post('/supervise/takeover', { callId, monitorId }),
  release: ({ callId, monitorId }) => post('/supervise/release', { callId, monitorId }),
  whisper: ({ callId, text }) => post('/supervise/whisper', { callId, text }),
});
monitor.on('state', (m) => console.log(m.state)); // listening, takeover, ended
await monitor.takeover(); // pide el micrófono y habla con quien llamó
await monitor.whisper('Ofrezca el viernes a las 9:30.');
await monitor.release(); // la IA vuelve a hablar
monitor.leave();

3. O en el teléfono listo: con las funciones en supervisor, una llamada que contestó la IA muestra “Contestada por la IA” y el botón Escuchar; la supervisión tiene Tomar la llamada, Devolver a la IA, Silenciar, Salir y un campo de instrucción para la IA.

const element = document.querySelector('voza-phone');
element.supervisor = {
  token: (call) => post('/supervise/token', { callId: call.id }), // tu servidor devuelve {token, ice_servers}
  takeover: ({ callId, monitorId }) => post('/supervise/takeover', { callId, monitorId }),
  release: ({ callId, monitorId }) => post('/supervise/release', { callId, monitorId }),
  whisper: ({ callId, text }) => post('/supervise/whisper', { callId, text }),
};
// o, para una llamada que elegiste:
element.monitor(callId, { token: 'mon_…', iceServers });

Llamadas por la API

Para quien prefiere su propio cliente WebRTC: las mismas acciones y eventos de la API de llamadas de Meta. Voza no graba las llamadas.

POST /v1/numbers/{id}/calls
AcciónCuerpo
connectLlama a to con la oferta SDP del navegador en session (sdp_type: offer). Responde {"calls": [{"id": "…"}]}; la respuesta SDP llega en el evento connect.
pre_acceptPara una llamada recibida: la respuesta SDP del navegador (sdp_type: answer), para que el audio se conecte de inmediato.
acceptContesta la llamada en WhatsApp. En los números oficiales, envía de nuevo la misma respuesta SDP en session.
reject, terminateRechaza o finaliza la llamada call_id.
{
  "action": "pre_accept",
  "call_id": "wacid.ABGG…",
  "session": { "sdp_type": "answer", "sdp": "v=0\r\no=- …" }
}

Permiso para llamar

GET /v1/numbers/{id}/call-permissions/{wa_id}

Si el número puede llamar al contacto ahora. En un número oficial, es la respuesta de la consulta call_permissions de Meta; en un número por código QR, siempre {"permission": {"status": "granted"}}, porque no hay permiso que pedir.

{
  "messaging_product": "whatsapp",
  "permission": { "status": "temporary", "expiration_time": 1761236400 },
  "actions": [{ "action_name": "start_call", "can_perform_action": true, "limits": [ … ] }]
}

status es granted, temporary (vence en expiration_time) o no_permission. wa_id es el número del contacto, solo con dígitos.

Eventos

Los eventos llegan al webhook del número con field: calls, en el sobre de Meta: calls[] con event: connect (la oferta SDP de una llamada recibida, o la respuesta de una llamada realizada) y event: terminate (con duration cuando fue contestada), y statuses[] de una llamada realizada (RINGING, ACCEPTED, REJECTED). direction es USER_INITIATED en una llamada recibida y BUSINESS_INITIATED en una realizada.

"value": {
  "messaging_product": "whatsapp",
  "metadata": { "display_phone_number": "5511987654321", "phone_number_id": "NUMBER_ID" },
  "contacts": [{ "wa_id": "5521999990000", "profile": { "name": "Bruno" } }],
  "calls": [{
    "id": "wacid.ABGG…",
    "from": "5521999990000",
    "to": "5511987654321",
    "event": "connect",
    "direction": "USER_INITIATED",
    "timestamp": "1761150000",
    "session": { "sdp_type": "offer", "sdp": "v=0\r\no=- …" }
  }]
}

Cuando la llamada termina:

"calls": [{ "id": "wacid.ABGG…", "event": "terminate", "direction": "USER_INITIATED", "status": "COMPLETED", "duration": 126, … }]

Registro de llamadas

GET /v1/numbers/{id}/calls

Las llamadas del número, de las más recientes a las más antiguas: ?limit= de 1 a 200, 50 por defecto.

{
  "data": [{
    "id": 981,
    "call_id": "wacid.ABGG…",
    "direction": "incoming",
    "contact": "5521999990000",
    "contact_name": "Bruno",
    "status": "completed",
    "end_reason": null,
    "end_detail": null,
    "started_at": "2026-10-22T15:20:00Z",
    "answered_at": "2026-10-22T15:20:04Z",
    "ended_at": "2026-10-22T15:22:10Z",
    "duration": 126,
    "answered_by": "agent",
    "agent": { "agent_ref": "agent_42", "session_id": "…", "seconds": 124 }
  }]
}
CampoDescripción
call_idEl id de la llamada en WhatsApp, el mismo de los eventos y las acciones.
statusdialing, ringing, active, completed, missed, rejected, no_answer o failed.
end_reasonchannel_limit: rechazada por superar el límite de llamadas simultáneas. agent_unavailable: rechazada porque el agente de voz no pudo atenderla. lost: estuvo más de 6 horas sin un final y se cerró como failed. Vacío en las demás.
end_detailCon agent_unavailable, por qué el agente rechazó la llamada (vea Quién atiende la llamada). Vacío en las demás.
answered_atCuándo fue contestada; vacío en una llamada no contestada. Los horarios están en UTC.
durationLos segundos de conversación; vacío en una llamada no contestada.
answered_byQuién atendió: webphone, agent (el agente de voz) o api (su sistema); en una llamada saliente, quién llamó. Vacío en una llamada que nadie atendió.
agentEn una llamada del agente de voz: agent_ref, la sesión en el motor y los segundos de la IA (sin el tiempo en que un supervisor tenía la llamada).

API del teléfono web

Lo que el SDK usa por debajo, para quien hace su propio teléfono en el navegador. Estas direcciones aceptan el token del teléfono web, no la clave de API, y responden a páginas de cualquier dominio (CORS). El token va en Authorization: Bearer wp_… o, en el flujo de eventos, en ?token=, porque EventSource no envía encabezados.

GET /v1/webphone/session
{
  "session_id": "01jbk5…",
  "label": "Ana",
  "number": { "id": "01jbk3…", "name": "Tienda Centro", "phone": "5511987654321", "status": "connected" },
  "ice_servers": [{ "urls": ["turn:turn.cloudflare.com:3478?transport=udp", …], "username": "…", "credential": "…" }],
  "expires_at": "2026-10-23T01:00:00Z"
}

ice_servers va directo a new RTCPeerConnection({ iceServers }). Pide la sesión de nuevo antes de cada llamada: las credenciales del TURN vencen.

GET /v1/webphone/events

Las llamadas del número, por Server-Sent Events. El evento calls trae el mismo value del webhook de llamadas (contacts, calls y statuses, con el SDP). El evento claimed indica que alguien atendió una llamada, con call_id, session_id y by (webphone, agent o api); si el session_id no es el suyo, se atendió en otro lugar.

const events = new EventSource('https://www.vozaapi.com/v1/webphone/events?token=wp_…');
events.addEventListener('calls', (event) => {
  const { calls, statuses } = JSON.parse(event.data);
});
events.addEventListener('claimed', (event) => {
  const { call_id, session_id, by } = JSON.parse(event.data);
});
POST /v1/webphone/calls

Las acciones de llamada en el número del token, con el mismo cuerpo de POST /v1/numbers/{id}/calls. Cuando varios teléfonos reciben la misma llamada, el primer pre_accept o accept se queda con ella, y los demás reciben 409 call_answered_elsewhere. Lo mismo vale frente al agente de voz y a su sistema con la clave de API (vea Agentes de voz).

Agentes de voz

Un agente de voz atiende las llamadas de un número con IA: escucha, piensa y habla. Funciona en los números por código QR y en los oficiales, con la misma API. La lógica es de su producto: Voza no guarda prompt ni configuración, solo qué agente de su producto atiende el número y dónde preguntar. Al comienzo de cada llamada, el motor de agentes de Voza le pide la configuración a su producto, y las herramientas (agenda, base de conocimiento, mensajes) también son llamadas a su producto. Una persona siempre puede escuchar, orientar a la IA y tomar la llamada.

Voza no guarda el contenido: la transcripción y el resumen van a su producto en los eventos, y solo quedan en Voza hasta la entrega. El motor de agentes guarda solo el audio de las frases fijas del agente (el saludo, las frases de espera y los mensajes de handoff), para reproducirlas sin esperar la voz sintética; una frase que no suena en 30 días se elimina, y eliminar la cuenta elimina las suyas. Las claves de los proveedores de IA son las de su cuenta.

El agente de un número

PUT /v1/numbers/{id}/agent
{
  "mode": "always",
  "fallback_after_seconds": 15,
  "agent_ref": "agent_42",
  "callback_url": "https://tu-sistema.com/voza/agents",
  "events_url": "https://tu-sistema.com/voza/events"
}
CampoDescripción
modealways: el agente atiende toda llamada entrante. fallback: los teléfonos web suenan primero y, si nadie atiende en fallback_after_seconds (de 5 a 120, 15 por defecto), atiende el agente. off: el agente solo atiende las llamadas que su producto le entregue.
agent_refEl agente en su producto (hasta 255 caracteres). Voza lo repite en cada solicitud y evento.
callback_urlDonde el motor pide la configuración (agent.config) y llama a las herramientas (tool.call). https y público.
events_urlOpcional: adónde van los eventos del agente. Sin ella, van al webhook del número.

La primera vez, la respuesta trae agent.secret: firma las solicitudes del motor a su producto y los eventos enviados a events_url. Guárdelo: GET en la misma dirección muestra la configuración sin él. POST /v1/numbers/{id}/agent/secret crea uno nuevo (el anterior deja de valer en el acto) y DELETE apaga el agente y olvida el secreto.

{ "agent": { "mode": "always", "fallback_after_seconds": 15, "agent_ref": "agent_42", "callback_url": "https://…/voza/agents", "events_url": "https://…/voza/events", "secret": "agsec_…" } }

Quién atiende la llamada

Una llamada entrante queda con quien atienda primero: un teléfono web, el agente o su sistema con la clave de API (pre_accept o accept en POST /v1/numbers/{id}/calls). Los demás reciben 409 call_answered_elsewhere, y los teléfonos web reciben el evento claimed con by: webphone, agent o api. El registro de llamadas muestra quién atendió en answered_by.

Con el agente en always, en cuanto llega la llamada el agente se queda con ella y el motor responde a la oferta SDP; los teléfonos web la ven sonar y, a continuación, el claimed. El webhook del número recibe los eventos calls como siempre. Si el motor no puede atender (su producto no envió la configuración y no había una guardada, falta una clave de IA…), la llamada se rechaza y queda en el registro con end_reason: agent_unavailable. En fallback, sigue sonando en los teléfonos web.

Tu producto también puede rechazar una llamada: si responde al agent.config con 409 o 410 y {"error": {"code": "agent_unavailable"}}, el motor no usa la configuración guardada y no atiende. En always, la llamada se rechaza (end_reason: agent_unavailable); en fallback, sigue sonando en los teléfonos web; entregada por la API, la respuesta es 409 agent_unavailable.

Una cuenta bloqueada (prueba terminada, suscripción terminada o suspendida) no tiene agente, como no tiene API ni teléfono web: la llamada llega al webhook del número y sigue sonando hasta que quien llama desiste.

Cuando el agente no puede quedarse con una llamada que suena, su producto recibe el evento call.agent.declined, con el motivo en reason, el código del rechazo en detail, el modo del número en mode y, en outcome, qué pasó con la llamada: rejected (rechazada, en always) o ringing (sigue sonando en los teléfonos web, en fallback). Una llamada rechazada queda en el registro con end_reason: agent_unavailable y el motivo en end_detail. Una llamada entregada por la API no genera el evento: su producto recibe el error.

reasonCuándo ocurre
product_refusedTu producto rechazó la llamada (respondió al agent.config con 409 o 410 y agent_unavailable).
config_unavailableSu producto no envió una configuración válida a tiempo, y el motor no tenía una guardada.
missing_provider_keyFalta la clave de un proveedor de IA que usa el agente.
invalid_offerEl motor no pudo responder a la oferta SDP.
at_capacity, drainingEl motor de agentes está en su límite de llamadas, o reiniciando (Voza vuelve a pedir una vez, dentro del mismo plazo).
account_limitLa cuenta ya tiene todas las sesiones de agente simultáneas que puede tener (limits.agent_sessions en GET /v1/account).
runtime_unavailable, runtime_errorEl motor de agentes no respondió a tiempo, o falló.
whatsapp_refusedMeta o el motor de QR rechazó la respuesta del agente (pre_accept).
errorOtra falla de Voza.

Cada cuenta tiene un límite de sesiones de agente al mismo tiempo, sumando las llamadas que atienden los agentes y las llamadas de prueba, y un límite menor solo para las llamadas de prueba: limits.agent_sessions y limits.agent_tests en GET /v1/account (10 y 2 por defecto; Voza fija los de una cuenta socia). Por encima de él, una llamada en always se rechaza (account_limit), una en fallback sigue sonando en los teléfonos web, y la API responde 429 agent_limit_reached.

POST /v1/numbers/{id}/calls/{call_id}/agent

Entrega al agente una llamada entrante que todavía está sonando: su producto decide llamada a llamada. {"agent_ref": "…"} es opcional (otro agente de su producto). Responde {"success": true, "call": {…}}, o 409 call_answered_elsewhere si alguien ya atendió y 409 call_not_ringing si ya terminó. call_id es el id de la llamada en WhatsApp; su id en Voza también vale.

Lo que el motor le pide a su producto

Solicitudes POST a la callback_url, con X-Voza-Event (agent.config o tool.call), X-Voza-Timestamp (segundos Unix) y X-Voza-Signature: sha256=…, el HMAC-SHA256 de {timestamp}.{cuerpo} (el timestamp, un punto y el cuerpo tal como llegó) con el secreto del agente. Comprueba la firma y rechaza un X-Voza-Timestamp a más de 5 minutos de tu reloj: así una solicitud capturada no puede repetirse después.

$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
$valid = hash_equals($expected, $signature) && abs(time() - (int) $timestamp) <= 300;

Al comienzo de cada llamada, la configuración (responda en hasta 2 segundos):

{ "type": "agent.config", "agent_ref": "agent_42", "call": { "id": "01jbk5…", "direction": "incoming", "test": false }, "number": { "id": "NUMBER_ID", "phone": "5511987654321", "type": "qr" }, "contact": { "wa_id": "5521999990000", "name": "Bruno" } }
{
  "version": "42.3",
  "language": "pt-BR",
  "greeting": { "text": "Hola, habla la asistente virtual de la Clínica Sonrisa. ¿En qué puedo ayudarle?", "interruptible": false },
  "instructions": "Agende, reprograme y cancele citas. Confirme la fecha en voz alta.",
  "guardrails": ["Nunca dé consejos médicos."],
  "voice": { "provider": "elevenlabs", "voice_id": "…", "speed": 1.0 },
  "stt": { "provider": "deepgram", "language": "multi" },
  "llm": { "provider": "anthropic", "model": "claude-haiku-5-5" },
  "turn": { "interruption": "medium", "silence_ms": 600 },
  "tools": [{
    "name": "find_slots",
    "description": "Horarios libres en la agenda",
    "parameters": { "type": "object", "properties": { "day": { "type": "string" } }, "required": ["day"] },
    "filler": "Un momento, voy a ver la agenda."
  }],
  "handoff": {
    "on_request": true,
    "message": "Le paso con el equipo, un momento.",
    "timeout_seconds": 45,
    "timeout_message": "Nadie del equipo puede atender ahora. ¿Puedo tomar un recado?",
    "timeout_action": "resume"
  },
  "error_message": "Disculpe, tengo un problema técnico. Le paso con el equipo.",
  "max_duration_seconds": 900
}

El motor guarda la última configuración de cada agente por su version: si su producto no responde a tiempo, usa la guardada. Cada herramienta se llama con {"type": "tool.call", "call_id", "agent_ref", "tool", "arguments"} y tiene 8 segundos para responder {"result": …}, con say (una frase a decir), end_call: true o handoff: true opcionales. La base de conocimiento es una herramienta como cualquier otra.

Cuando un proveedor falla

Si la transcripción, la IA o la voz fallan en medio de una llamada (el proveedor rechazó la clave de la cuenta, sigue fallando, o la IA no responde a un turno en 20 segundos), quien llama no queda en silencio: su producto recibe call.agent.error, el agente dice error_message (o, sin ella, el mensaje del handoff; si falló la propia voz, solo una frase ya guardada, o si no un tono) y pasa la llamada al equipo, si el agente tiene handoff.on_request, o la termina (reason: provider_error). La IA no vuelve a atender en esa llamada: si el equipo la devuelve, o nadie la toma, termina.

{ "service": "llm", "provider": "anthropic", "status": 401, "category": "authentication", "action": "handoff" }

service es stt, llm o tts; status, el estado HTTP del proveedor, cuando lo hay; category, el tipo de falla (authentication, rate_limit, quota, server, timeout…); action, handoff o end.

Eventos del agente

Van a la events_url del agente, firmados con el secreto del agente, o al webhook del número, firmados con el secreto del webhook. Tienen object: voza y el nombre del evento en event y en X-Voza-Event. Salen en una cola propia, y los de una llamada salen en orden, uno después del otro: un evento que falló retiene los siguientes de la misma llamada hasta el nuevo intento. Los nuevos intentos son después de 5, 15 y 30 segundos, por hasta 2 minutos; call.agent.ended (con la conversación entera), call.agent.error y call.agent.declined siguen después, por el plazo de los eventos de mensaje.

{
  "object": "voza",
  "event": "call.transcript",
  "event_id": "5f2c…",
  "number": { "id": "NUMBER_ID", "name": "Tienda Centro", "external_ref": null, "status": "connected", "status_reason": null, "phone": "5511987654321" },
  "call": { "id": "01jbk5…", "call_id": "wacid.ABGG…", "direction": "incoming", "contact": "5521999990000", "contact_name": "Bruno", "agent_ref": "agent_42", "agent_session_id": "…" },
  "test": false,
  "data": { "speaker": "caller", "text": "Quiero reprogramar la cita del viernes.", "start": 4.2, "end": 6.9 },
  "timestamp": 1761150012,
  "timestamp_ms": 1761150012345
}
EventoCuándoEn data
call.agent.startedEl agente atendió.call, number, contact, agent_ref y la versión de la configuración (config_version).
call.agent.stateEn cada cambio.state: listening, thinking, speaking, handoff, handoff_timeout (nadie tomó la llamada a tiempo) o supervised.
call.transcriptEn cada frase completa.speaker (caller, agent o supervisor), text, start y end en segundos desde que se atendió.
call.agent.tool_callEn cada herramienta.name, duration_ms y result (ok, timeout, error); sin los argumentos.
call.metricsEn cada turno.Las latencias en milisegundos: stt_ms, llm_ms (hasta la primera palabra), tts_ms y end_to_end_ms.
call.supervisionUn supervisor entra, cambia de modo, susurra o sale.action, monitor_id y mode.
call.agent.errorUn proveedor falló en medio de la llamada.service, provider, status, category y action (vea Cuando un proveedor falla).
call.agent.declinedEl agente no pudo quedarse con una llamada que sonaba.reason, detail, mode y outcome (vea Quién atiende la llamada).
call.agent.endedEl final.reason, duration_seconds, los segundos de la IA (ai_seconds) y los de un supervisor con la llamada (supervised_seconds), la transcripción completa (transcript), las herramientas usadas (tools) y el uso (usage).

timestamp es el instante del evento en segundos, y timestamp_ms, el mismo instante en milisegundos: úsalo para ordenar eventos del mismo segundo.

event_id es el mismo en cada intento: úselo para no procesar un evento dos veces. Las llamadas atendidas por el agente y los segundos de la IA entran en el uso (agent_calls y agent_seconds en GET /v1/account/usage).

El reason de call.agent.ended: remote_ended (quien llamó colgó), agent o tool (el agente la terminó), handoff_timeout, max_duration, max_call_duration, idle, media_closed, connect_timeout, accept_failed, provider_error (un proveedor falló), agent_removed, number_removed y account_blocked (se quitó el agente o el número, o se bloqueó la cuenta: la llamada termina en el acto), runtime_lost (el motor de agentes cayó en medio de la llamada: Voza termina la llamada y envía este evento, sin la transcripción) o shutdown.

Supervisión

Quién puede supervisar lo decide su producto; Voza solo verifica la clave de API. Terminar la llamada sigue siendo la acción terminate.

POST /v1/numbers/{id}/calls/{call_id}/monitor

Un token corto (mon_…, 5 minutos, una vez) para el navegador del supervisor, con los ice_servers. El navegador envía su oferta SDP a POST /v1/monitor/session con Authorization: Bearer mon_… y recibe la respuesta del motor y el monitor_id. mode es listen (escucha; el predeterminado) o takeover (entra hablando). Esta dirección acepta páginas de cualquier dominio (CORS).

{ "session": { "sdp_type": "offer", "sdp": "v=0\r\no=- …" }, "mode": "listen" }
{ "monitor_id": "mon_1", "call_id": "wacid.ABGG…", "session": { "sdp_type": "answer", "sdp": "v=0\r\no=- …" } }
DirecciónCuerpoQué hace
POST …/calls/{call_id}/whisper{"text": "Ofrezca el viernes a las 9:30."}El texto entra en el contexto de la IA; quien llamó no lo oye.
POST …/calls/{call_id}/takeover{"monitor": "mon_1"}La IA deja de hablar y el micrófono de ese supervisor pasa a la llamada.
POST …/calls/{call_id}/release{}Devuelve la llamada a la IA.
DELETE …/calls/{call_id}/monitor/{monitor_id}Quita a un supervisor de la llamada; si la tenía, la IA vuelve a hablar.

En el navegador, el SDK del teléfono web hace la parte del supervisor: consulta Supervisar una llamada de la IA.

Llamada de prueba

POST /v1/agents/test-sessions

Converse con un agente desde el navegador, sin WhatsApp: envíe {"agent_ref", "callback_url"} y reciba {id, token, ice_servers, expires_at}. El navegador envía la oferta SDP a POST /v1/agents/test-sessions/connect con Authorization: Bearer agt_… (una vez) y recibe la respuesta del motor. Con number_id, el motor firma con el secreto del agente de ese número, y los eventos salen con "test": true adonde van los del número; sin él, la respuesta trae un secret solo de esta llamada, y los eventos no se envían. DELETE /v1/agents/test-sessions/{id} la termina.

Una llamada de prueba dura como máximo 10 minutos y cuenta en las sesiones de agente de la cuenta: por encima de limits.agent_tests llamadas de prueba al mismo tiempo, connect responde 429 test_session_limit_reached (y 429 agent_limit_reached por encima de limits.agent_sessions). El token puede usarse de nuevo después de un rechazo así.

Voz y claves de IA

PUT /v1/account/ai-keys

Guarda, cifradas, las claves de los proveedores de IA de la cuenta: anthropic, deepgram, elevenlabs y cartesia. Solo cambian los proveedores enviados, y null elimina una clave. Solo una clave de API creada por un propietario de la cuenta puede cambiarlas (si no, 403 owner_only). Nunca se devuelven: la respuesta, como GET, solo dice cuáles existen.

Antes de guardar, Voza verifica cada clave nueva en el propio proveedor, en una dirección que no gasta nada (la lista de modelos, de proyectos o de voces). Si rechaza una clave (401), no se guarda ninguna clave de la solicitud, y la respuesta es 422 invalid_provider_key, con los proveedores en meta.providers. Un proveedor caído, lento, o que acepta la clave sin el permiso de esa lista, no impide la clave.

{ "anthropic": "sk-ant-…", "deepgram": "…" }
{ "anthropic": true, "deepgram": true, "elevenlabs": false, "cartesia": false }
POST /v1/tts

{"provider", "voice_id", "text", "speed"} → el audio (audio/mpeg, o audio/wav con "format": "wav"), con la clave del proveedor guardada en la cuenta. Sirve para escuchar el saludo antes de publicarlo. El texto no queda guardado; una frase fija que una llamada del agente ya guardó (la misma voz y el mismo texto) vuelve sin esperar (X-Voza-Cache: hit).

GET /v1/voices?provider=elevenlabs

Las voces de la cuenta en un proveedor (elevenlabs o cartesia), leídas con la clave guardada en la cuenta, así que las voces clonadas también vienen. Cada voz trae id (el voice_id de la configuración y de /v1/tts), name, languages, gender y accent cuando el proveedor lo dice, y preview_url. La lista se guarda una hora, por cuenta y proveedor.

{ "provider": "elevenlabs", "data": [{ "id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel", "languages": ["en", "pt-BR"], "gender": "female", "accent": "american", "preview_url": "https://…/preview.mp3" }] }

Errores de los agentes

EstadoCódigoCuándo ocurre
401invalid_monitor_token, invalid_test_tokenEl token de supervisión o de la llamada de prueba está ausente, es inválido o venció.
403owner_onlySolo una clave de API creada por un propietario guarda las claves de IA.
404monitor_not_foundNo hay un supervisor con ese id en la llamada.
409agent_not_configuredEl número no tiene un agente.
409agent_unavailableTu producto rechazó la llamada (respondió al agent.config con 409 o 410 y agent_unavailable).
409call_not_ringingLa llamada ya no está sonando.
409call_not_with_agentLa llamada no está con el agente: otra persona la atendió, terminó o todavía no fue atendida.
409token_usedEl token de supervisión o de la llamada de prueba ya fue usado.
409too_many_monitors, not_in_controlLa llamada ya tiene todos los supervisores, u otro supervisor la tomó.
422missing_provider_keyFalta la clave de un proveedor que usa el agente: guárdela en PUT /v1/account/ai-keys.
422invalid_offerEl motor no pudo responder a la oferta SDP.
422invalid_provider_keyUn proveedor rechazó la clave enviada en PUT /v1/account/ai-keys.
429agent_limit_reached, test_session_limit_reachedLa cuenta ya tiene todas las sesiones de agente, o llamadas de prueba, simultáneas que puede tener.
502agent_config_unavailableSu producto no envió la configuración a tiempo, y el motor no tenía una guardada.
502tts_failedEl proveedor de voz no generó el audio.
502voices_failedEl proveedor de voz no devolvió la lista de voces (una clave rechazada, por ejemplo).
503agents_unavailable, agents_busyEl motor de agentes no respondió o está en su límite de llamadas. meta.reason trae el motivo que dio el motor.

Cuenta, plan y uso

GET /v1/account
{
  "id": 42,
  "name": "Tu Empresa",
  "status": "active",
  "trial_ends_at": null,
  "partner": false,
  "plan": "pro",
  "subscription": { "status": "active", "currency": "brl", "renews_at": "2026-11-23T14:30:00Z", "ends_at": null },
  "limits": { "numbers": 5, "channels": 10, "requests_per_minute": 600, "agent_sessions": 10, "agent_tests": 2 },
  "webhook_retry_hours": 24
}
CampoDescripción
statustrial (prueba gratis), active (activa), suspended (suspendida) o canceled (suscripción finalizada).
planstart, pro, scale, o vacío sin suscripción.
subscriptionLa suscripción en Stripe: status (active, past_due cuando un pago falló, canceled…), la moneda y las fechas. Vacío sin suscripción.
limitsCuántos números conecta la cuenta, cuántas llamadas simultáneas puede tener, cuántas solicitudes por minuto hace en la API y cuántas sesiones de agente de voz tiene al mismo tiempo (agent_sessions, de las cuales hasta agent_tests llamadas de prueba).
webhook_retry_hoursDurante cuántas horas se reintenta un evento de mensaje que falló: 24, o el plazo acordado con Voza, hasta 168 (7 días).
partnerUna cuenta socia, de otro producto, con límites acordados con Voza: sus llamadas no se rechazan por el límite de llamadas simultáneas, porque el producto limita las de sus clientes.

La suscripción se hace en el panel (Plan y uso), con tarjeta, a través de Stripe. Cuando termina, la API responde subscription_inactive hasta que la cuenta se suscriba de nuevo.

GET /v1/account/usage?month=2026-10
{
  "month": "2026-10",
  "totals": { "messages_sent": 1840, "messages_received": 2311, "calls": 96, "call_seconds": 20412, "agent_calls": 40, "agent_seconds": 7310 },
  "numbers": [
    { "number_id": "01jbk3…", "name": "Tienda Centro", "messages_sent": 1200, "messages_received": 1500, "calls": 60, "call_seconds": 14000, "agent_calls": 40, "agent_seconds": 7310 }
  ]
}

El uso del mes en UTC, en total y por número (number_id vacío para un número ya eliminado). Sin month, el mes actual. agent_calls y agent_seconds son las llamadas que atendió un agente de voz y los segundos de la IA en ellas (sin el tiempo en que un supervisor tenía la llamada).

OpenAPI y Postman

Toda la API está descrita en OpenAPI 3.1: https://www.vozaapi.com/docs/openapi.json?lang=es. Impórtala en Postman o Insomnia, o genera un cliente en tu lenguaje.

Los textos de la especificación llegan en el idioma de ?lang=: pt-BR, en o es.

Estado de Voza

El estado de Voza en vivo (sitio y API, base de datos, webhooks y correos, nuevos intentos, los números por código QR y los agentes de voz) está en https://www.vozaapi.com/status. Para un monitor de disponibilidad, lo mismo en JSON: https://www.vozaapi.com/status.json, que responde 503 cuando algún servicio está caído o atrasado.

{
  "status": "operational",
  "checked_at": "2026-10-27T15:00:00Z",
  "components": [{ "key": "engine", "name": "Números por código QR", "status": "operational", "message": null }]
}

status es operational, degraded o down. Los números oficiales dependen también de la API de Meta, que esta página no sigue.

Novedades

Los cambios que puedan romper tu integración se avisan con anticipación y entran en una nueva versión de la dirección (/v2); la /v1 solo gana campos y eventos nuevos.