Llamadas
La API de Llamadas te permite listar el historial de llamadas, ver detalles de llamadas con transcripciones, descargar grabaciones, iniciar llamadas salientes y ejecutar campanas de llamadas por lotes.
Cada endpoint de esta seccion acepta ambos metodos de autenticacion: JWT Bearer y clave de API (X-API-Key). Esto significa que una integracion automatizada (por ejemplo, un manejador que recibe el webhook de fin de llamada) puede listar llamadas y descargar grabaciones usando solo una clave de API.
Listar Llamadas
GET /calls
Devuelve una lista paginada de registros de llamadas, ordenados por hora de inicio (mas recientes primero).
Parametros de Consulta
| Parametro | Tipo | Por defecto | Descripcion |
|---|---|---|---|
page | integer | 1 | Numero de pagina |
page_size | integer | 20 | Elementos por pagina (maximo 100) |
direction | string | -- | Filtrar por inbound o outbound |
status | string | -- | Filtrar por estado de llamada (ej. completed, failed) o resultado (ej. voicemail, caller_hangup, answered_no_session) |
search | string | -- | Buscar por numero de telefono (origen o destino) |
Respuesta
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"call_sid": "CA1234567890abcdef",
"direction": "inbound",
"from_number": "+15551234567",
"to_number": "+15559876543",
"start_time": "2026-03-01T14:30:00Z",
"end_time": "2026-03-01T14:32:45Z",
"duration": 165.0,
"status": "completed",
"outcome": "completed",
"error_reason": null,
"created_at": "2026-03-01T14:30:00Z",
"agent_name": "Support Agent"
}
],
"total": 142,
"page": 1,
"page_size": 20
}
Campos del Registro de Llamada
| Campo | Tipo | Descripcion |
|---|---|---|
id | uuid | ID interno del registro de llamada |
call_sid | string | Twilio Call SID |
direction | string | inbound o outbound |
from_number | string | Numero de telefono del llamante |
to_number | string | Numero de telefono llamado |
start_time | datetime | Marca de tiempo de inicio de llamada |
end_time | datetime | null | Marca de tiempo de fin de llamada |
duration | float | null | Duracion de la llamada en segundos. Se concilia con el audio grabado, de modo que no puede superar con mucho el audio que la llamada realmente transporto (ver mas abajo). |
status | string | Estado de la llamada (completed, failed, in-progress, queued) |
outcome | string | null | Resultado de la llamada (completed, caller_hangup, no_answer, timeout, transferred, voicemail, invalid_number, answered_no_session, failed). timeout indica que la llamada la terminamos nosotros y no el llamante: un límite de silencio, un límite de duración máxima, o una línea que dejó de transmitir audio; caller_hangup es una desconexión real del llamante; voicemail indica que contestó un contestador automático, ya sea porque lo detectó la detección de contestador o porque el agente reconoció el mensaje; answered_no_session indica que la llamada fue contestada y facturada pero no se registró ninguna conversación y se desconoce el motivo (ver más abajo). |
error_reason | string | null | Descripcion del error si la llamada fallo |
created_at | datetime | Marca de tiempo de creacion del registro |
agent_name | string | null | Nombre del agente que atendio la llamada |
Llamadas Contestadas Sin Conversacion
outcome: "answered_no_session" marca una llamada que el proveedor de telefonia contesto y finalizo con normalidad, pero para la que el pipeline de voz nunca registro una sesion. El registro lleva una duration, start_time y end_time reales, y status: "completed", mientras que transcript, recording_url y call_analysis quedan en null porque no se capturo nada. error_reason lo explica en lenguaje claro.
Tratala como un caso propio en lugar de mezclarla con una llamada sin respuesta:
- No es
no_answernibusy. Esos significan que la llamada nunca conecto. Esta si conecto, y la persona al otro lado escucho lo que la linea transmitiera. - Tampoco es
voicemail. Una llamada que contesto un contestador automatico se etiqueta comovoicemail, porque algo identifico la maquina.answered_no_sessiones lo que queda cuando nada lo hizo, asi que el motivo de la conversacion ausente se desconoce. - Rellamar es una decision tuya. Puede que ya se haya hablado con el destinatario, y no hay transcripcion para comprobarlo.
- El webhook
call.completedse dispara para ella como para cualquier otra llamada terminal, con el mismooutcomey contranscriptyrecording_urlennull.
Relacion entre duration y la Grabacion
duration mide la llamada desde que se abrio la sesion hasta que se cerro, por lo que normalmente es uno o dos segundos mas larga que la grabacion: el establecimiento y el cierre quedan a ambos lados del audio.
De vez en cuando una linea deja de transportar audio sin llegar a avisar de que la llamada ha terminado. Cuando eso ocurre cerramos la sesion poco despues de que el audio se detenga, el resultado es timeout, y duration se concilia con la grabacion para que el registro - y el consumo que alimenta - refleje el audio que la llamada realmente transporto y no el tiempo que la linea permanecio abierta. Las llamadas cuya duration ya coincide con su grabacion nunca se ajustan.
Obtener Detalle de Llamada
GET /calls/{call_id}
Devuelve una llamada individual con detalles completos incluyendo la transcripcion de la conversacion y metricas de uso de proveedores.
Respuesta
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"call_sid": "CA1234567890abcdef",
"direction": "inbound",
"from_number": "+15551234567",
"to_number": "+15559876543",
"start_time": "2026-03-01T14:30:00Z",
"end_time": "2026-03-01T14:32:45Z",
"duration": 165.0,
"status": "completed",
"outcome": "completed",
"error_reason": null,
"created_at": "2026-03-01T14:30:00Z",
"agent_name": "Support Agent",
"transcript": [
{
"role": "user",
"content": "Hi, I need help with my order.",
"timestamp": "2026-03-01T14:30:08Z"
},
{
"role": "assistant",
"content": "Of course! Could you share your order number?",
"timestamp": "2026-03-01T14:30:11Z"
}
],
"provider_usage": {
"deepgram": { "type": "stt", "model": "nova-2" },
"openai": { "type": "llm", "model": "gpt-4o" },
"elevenlabs": { "type": "tts", "model": "eleven_turbo_v2" }
},
"recording_url": "call-CA1234567890abcdef.wav"
}
Campos Adicionales de Detalle
| Campo | Tipo | Descripcion |
|---|---|---|
transcript | array | null | Turnos de conversacion con role, content y timestamp, mas interrupted en un turno que la persona que llama corto (ver abajo) |
provider_usage | object | null | Nombres de proveedores y modelos utilizados durante la llamada |
recording_url | string | null | Nombre del archivo de grabacion WAV (si la grabacion estaba habilitada) |
Marcas de Tiempo de los Turnos
Cada turno lleva el momento en que ocurrio, no el momento en que se escribio el registro, de modo que una transcripcion puede superponerse a la grabacion y reconstruir la llamada turno a turno.
Turnos Interrumpidos
Cuando la persona que llama habla por encima del agente y el agente se detiene a mitad de frase, ese turno registra unicamente el texto que la persona alcanzo a oir. Lleva "interrupted": true y su content termina con [cut off by caller]. El turno de la persona que interrumpio se prefija con [interrupting]:
"transcript": [
{ "role": "user", "content": "Can you book me a table?" },
{
"role": "assistant",
"content": "Sure, I can book that for [cut off by caller]",
"interrupted": true
},
{ "role": "user", "content": "[interrupting] actually make it Friday" },
{ "role": "assistant", "content": "Friday it is. What time suits you?" }
]
La clave interrupted solo aparece en los turnos que fueron cortados, asi que tratala como opcional. De este modo la transcripcion de un turno interrumpido coincide con la grabacion de audio, en lugar de mostrar la respuesta mas larga que el agente habia generado.
El punto de corte es lo que realmente habia llegado a la persona que llama en el momento de interrumpir. El audio del agente sigue reproduciendose un instante despues de componer la respuesta, asi que un turno interrumpido en esa ventana tambien lleva "interrupted": true y se trunca en los mismos terminos.
Descargar Grabacion
GET /calls/{call_id}/recording
Descarga el archivo de grabacion WAV de una llamada. Devuelve 404 si no hay grabacion disponible.
El webhook call.completed incluye un recording_download_url que apunta a este endpoint. Descargalo con tu clave de API en la cabecera X-API-Key para obtener el audio.
Respuesta
200 OK con Content-Type: audio/wav y el contenido binario del archivo.
Iniciar Llamada Saliente
POST /calls
Encola una llamada saliente individual. La llamada se procesa de forma asincrona a 1 llamada por segundo por un worker en segundo plano. La respuesta se devuelve inmediatamente con un call_id y un call_sid provisional que se actualiza cuando Twilio asigna el SID real.
Este endpoint acepta autenticacion tanto por Bearer JWT como por API key.
Cuerpo de la Solicitud
{
"to_number": "+15551234567",
"from_number": "+15559876543",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"variables": {
"customer_name": "Jane Doe",
"company_name": "Globex"
}
}
Campos de la Solicitud
| Campo | Tipo | Requerido | Descripcion |
|---|---|---|---|
to_number | string | Si | Numero de telefono de destino en formato E.164 |
from_number | string | No | Numero de identificacion del llamante. Si se omite, se resuelve desde el numero asignado al agente o el valor predeterminado de la configuracion SIP. |
agent_id | string | No | UUID del agente que manejara la llamada. Si se omite, se usa el agente asignado a from_number. |
variables | object | No | Sustituciones por llamada para las variables personalizadas del agente objetivo, como un mapa de nombre de variable a valor de texto. Se aplica solo a esta llamada y nunca cambia los valores predeterminados guardados del agente. Cada clave debe ser una variable personalizada definida en el agente objetivo; el nombre de una variable integrada o una clave desconocida devuelve 422 con las claves ofensivas. |
A diferencia del editor del panel, que solo advierte sobre marcadores no reconocidos y aun así te deja guardar, este endpoint valida variables de forma estricta. Una clave que coincide con el nombre de una variable integrada (las integradas nunca son sustituibles) o que no es una variable personalizada definida en el agente objetivo se rechaza con 422, y la respuesta lista las claves ofensivas. Esto expone los errores de integración de forma explícita en lugar de descartar datos silenciosamente. Consulta Variables Dinámicas para el modelo de variables.
Respuesta
202 Accepted
{
"call_id": "550e8400-e29b-41d4-a716-446655440000",
"call_sid": "queued-550e8400e29b",
"status": "queued",
"deduplicated": false,
"dedupe_reason": null
}
El call_sid sera un valor provisional con prefijo queued- hasta que el worker en segundo plano inicie la llamada con Twilio.
Encabezados de la Solicitud
| Encabezado | Requerido | Descripcion |
|---|---|---|
Idempotency-Key | No | Tu propio identificador para este envio de llamada (maximo 255 caracteres). Repetir la misma clave devuelve la llamada original en lugar de marcar de nuevo. Consulta Supresion de Duplicados. |
Supresion de Duplicados
Encolar una llamada es "dispara y olvida", asi que una automatizacion que reintenta -- o que lanza una rafaga de disparos antes de que su propio registro de "ya llamado" se haya guardado -- podria llamar a la misma persona una vez por solicitud. Dos protecciones se aplican antes de la cola, y ambas son seguras ante rafagas de solicitudes simultaneas: solo una puede ganar, sin importar cuantas lleguen en el mismo instante.
Clave de idempotencia. Envia un encabezado Idempotency-Key con tu propio identificador del envio (un id de ejecucion del flujo, un id de fila, un UUID -- cualquier valor estable para esa llamada logica). La primera solicitud encola la llamada y vincula la clave a ella durante 24 horas. Cualquier repeticion de la misma clave dentro de esa ventana no encola nada y devuelve el call_id y el call_sid de la llamada original. La clave tiene alcance por cuenta y por endpoint, asi que el mismo valor puede reutilizarse de forma independiente en POST /calls y en POST /calls/batch. Solo se compara la clave, nunca el cuerpo de la solicitud.
Periodo de espera por numero. Incluso sin clave, una segunda llamada a un numero al que acabas de llamar se suprime durante un periodo de espera corto (60 segundos por defecto, configurable por despliegue y desactivable). Es una red de seguridad frente a tormentas de reintentos; no sustituye al envio de una clave de idempotencia.
Una solicitud suprimida no es un error. Devuelve 202 Accepted con los identificadores de la llamada ya encolada, mas:
| Campo | Tipo | Descripcion |
|---|---|---|
deduplicated | boolean | true cuando esta solicitud no encolo nada |
status | string | duplicate en lugar de queued |
dedupe_reason | string | null | idempotency_key o number_cooldown |
{
"call_id": "550e8400-e29b-41d4-a716-446655440000",
"call_sid": "queued-550e8400e29b",
"status": "duplicate",
"deduplicated": true,
"dedupe_reason": "idempotency_key"
}
Como la respuesta lleva el call_id original, un cliente que reintenta tras un tiempo de espera puede tratar con seguridad una respuesta duplicate como exito y seguir consultando la misma llamada. Vigila deduplicated en tu integracion: un flujo constante de true significa que tu automatizacion esta enviando mas disparos de los que pretende.
La primera llamada a cualquier numero, con o sin encabezado, se comporta exactamente igual que siempre.
Llamadas Salientes por Lotes
POST /calls/batch
Encola multiples llamadas salientes a la vez. Todas las llamadas comparten el mismo from_number y agent_id, y se procesan a 1 llamada por segundo.
Cuerpo de la Solicitud
{
"to_numbers": ["+15551111111", "+15552222222", "+15553333333"],
"from_number": "+15559876543",
"agent_id": "550e8400-e29b-41d4-a716-446655440000"
}
Campos de la Solicitud
| Campo | Tipo | Requerido | Descripcion |
|---|---|---|---|
to_numbers | array[string] | Si | Lista de numeros de telefono de destino |
from_number | string | No | Identificacion de llamante compartida para todas las llamadas |
agent_id | string | No | Agente que manejara todas las llamadas del lote |
Encabezados de la Solicitud
| Encabezado | Requerido | Descripcion |
|---|---|---|
Idempotency-Key | No | Tu propio identificador para este envio por lotes (maximo 255 caracteres). Repetir la misma clave devuelve el batch_id original y no encola nada. |
Respuesta
202 Accepted
{
"batch_id": "batch-abc123",
"total": 3,
"status": "processing",
"deduplicated": false,
"dedupe_reason": null,
"skipped_numbers": []
}
total es el numero de llamadas que esta solicitud encolo, que no siempre coincide con la longitud de to_numbers.
Supresion de Duplicados en un Lote
La supresion de duplicados tambien se aplica a los lotes, con una diferencia: el periodo de espera por numero se evalua numero a numero, asi que una lista con solapamiento sigue enviando los numeros nuevos.
Idempotency-Keyrepetida: no se encola nada. La respuesta lleva elbatch_idoriginal,total: 0,status: "duplicate"y todos los numeros solicitados enskipped_numbers.- Numeros en periodo de espera: el resto de la lista se encola con normalidad.
totalcuenta solo esos, los numeros suprimidos se listan enskipped_numbers, ydeduplicatedestruecondedupe_reason: "number_cooldown". Un numero repetido dos veces dentro del mismo cuerpo se marca una sola vez por el mismo motivo. - Todos los numeros en periodo de espera: no se encola nada y
statusesduplicate.
{
"batch_id": "batch-def456",
"total": 2,
"status": "processing",
"deduplicated": true,
"dedupe_reason": "number_cooldown",
"skipped_numbers": ["+15551111111"]
}
Obtener Estado del Lote
GET /calls/batch/{batch_id}
Consulta el progreso de un trabajo de llamadas por lotes.
Respuesta
{
"batch_id": "batch-abc123",
"total": 3,
"queued": 1,
"initiated": 1,
"failed": 1,
"calls": [
{ "to_number": "+15551111111", "status": "initiated", "call_sid": "CA..." },
{ "to_number": "+15552222222", "status": "queued", "call_sid": null },
{ "to_number": "+15553333333", "status": "failed", "call_sid": null, "error": "Invalid number" }
]
}