Enviar una grabación
Envía una grabación y lo que sepas de ella. Caller crea la llamada, la pone en cola para transcribirla y responde con su identificador. Una petición por llamada.
Petición
Sección titulada «Petición»El cuerpo es multipart/form-data: un formulario con el archivo y, si quieres, campos de texto. La única cabecera que tienes que poner es la de autenticación; la biblioteca que uses pone el Content-Type del formulario.
curl https://api.caller.ee/v1/calls \ -H "Authorization: Bearer $CALLER_API_KEY" \ -F "file=@grabaciones/pbx-000123.mp3" \ -F "external_id=pbx-000123" \ -F "agent_code=AG1143" \ -F "campaign=Retención" \ -F "started_at=2026-10-09T10:42:11+02:00" \ -F "customer_phone=+34600000000" \ -F "direction=inbound"import osimport requests
with open("grabaciones/pbx-000123.mp3", "rb") as audio: respuesta = requests.post( "https://api.caller.ee/v1/calls", headers={"Authorization": f"Bearer {os.environ['CALLER_API_KEY']}"}, files={"file": ("pbx-000123.mp3", audio, "audio/mpeg")}, data={ "external_id": "pbx-000123", "agent_code": "AG1143", "campaign": "Retención", "started_at": "2026-10-09T10:42:11+02:00", "customer_phone": "+34600000000", "direction": "inbound", }, timeout=120, )
cuerpo = respuesta.json()if not respuesta.ok: raise RuntimeError(f"{respuesta.status_code} {cuerpo['error']['code']}")print(cuerpo["id"], cuerpo["status"])// Node.js 18 o posterior: fetch, FormData y Blob vienen incluidos.import { readFile } from 'node:fs/promises';
const audio = new Blob([await readFile('grabaciones/pbx-000123.mp3')], { type: 'audio/mpeg' });
const formulario = new FormData();formulario.append('file', audio, 'pbx-000123.mp3');formulario.append('external_id', 'pbx-000123');formulario.append('agent_code', 'AG1143');formulario.append('campaign', 'Retención');formulario.append('started_at', '2026-10-09T10:42:11+02:00');formulario.append('customer_phone', '+34600000000');formulario.append('direction', 'inbound');
const respuesta = await fetch('https://api.caller.ee/v1/calls', { method: 'POST', headers: { Authorization: `Bearer ${process.env.CALLER_API_KEY}` }, body: formulario,});
const cuerpo = await respuesta.json();if (!respuesta.ok) throw new Error(`${respuesta.status} ${cuerpo.error.code}`);console.log(cuerpo.id, cuerpo.status);<?php$peticion = curl_init('https://api.caller.ee/v1/calls');curl_setopt_array($peticion, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CALLER_API_KEY')], CURLOPT_POSTFIELDS => [ 'file' => new CURLFile('grabaciones/pbx-000123.mp3', 'audio/mpeg', 'pbx-000123.mp3'), 'external_id' => 'pbx-000123', 'agent_code' => 'AG1143', 'campaign' => 'Retención', 'started_at' => '2026-10-09T10:42:11+02:00', 'customer_phone' => '+34600000000', 'direction' => 'inbound', ],]);
$cuerpo = json_decode(curl_exec($peticion), true);$estado = curl_getinfo($peticion, CURLINFO_RESPONSE_CODE);curl_close($peticion);
if ($estado >= 400) { throw new RuntimeException($estado . ' ' . $cuerpo['error']['code']);}echo $cuerpo['id'], ' ', $cuerpo['status'], PHP_EOL;Solo file es obligatorio. Los campos de texto se recortan a 300 caracteres.
| Campo | Tipo | Descripción |
|---|---|---|
file |
archivo | Obligatorio. La grabación. Ver formatos. |
external_id |
texto | El identificador de la llamada en tu sistema, de hasta 200 caracteres. Enviar dos veces el mismo no duplica la llamada. Ver reintentos. |
agent_code |
texto | El código del agente en tu centralita. Se compara con el Código en la centralita de cada agente de Equipo, sin distinguir mayúsculas, guiones, guiones bajos ni espacios. |
agent_name |
texto | El nombre del agente, tal como está en Equipo. Se usa si no hay código o si nadie lo tiene. |
campaign |
texto | El nombre de una campaña que ya exista en la cuenta. No distingue mayúsculas ni tildes. |
started_at |
fecha | Cuándo empezó la llamada: ISO 8601 con su desfase (2026-10-09T10:42:11+02:00) o tiempo Unix en segundos o en milisegundos. |
customer_phone |
texto | El teléfono del cliente. |
customer_name |
texto | El nombre del cliente. |
direction |
texto | inbound si la llamada fue entrante, outbound si fue saliente. |
language |
texto | El código del idioma de la llamada, para no dejarlo a la detección automática. Ver idioma. |
Lo que no envíes se lee del nombre del archivo
Sección titulada «Lo que no envíes se lee del nombre del archivo»Si faltan el agente, la fecha, el teléfono o la dirección, Caller intenta leerlos del nombre del archivo, igual que al subir a mano. Un campo enviado gana siempre al nombre. Las reglas están en Nombres de archivo.
El agente es obligatorio
Sección titulada «El agente es obligatorio»Todo lo demás es opcional, pero cada grabación tiene que decir de quién es, y esa persona tiene que estar ya en Equipo. Vale cualquiera de estas tres formas: agent_code, agent_name o el código del agente en el nombre del archivo.
- Si la grabación no dice de quién es, la respuesta es
422conagent_required. - Si lo dice pero nadie de Equipo tiene ese código o ese nombre, la respuesta es
422conunknown_agent.
En los dos casos la grabación no se guarda y no gasta minutos. Los agentes se dan de alta en la app, en Equipo, con el mismo código que usa tu centralita; cada uno ocupa una plaza de tu plan.
Con una centralita que ya nombra bien sus grabaciones, la petición mínima es el archivo y su external_id.
Cuándo un campo se ignora
Sección titulada «Cuándo un campo se ignora»Salvo el agente, que es obligatorio, la API no rechaza una petición por un dato que no reconoce: crea la llamada sin ese dato. Conviene saberlo, porque no verás un error.
| Situación | Qué pasa |
|---|---|
campaign no existe o está archivada |
La llamada entra sin campaña y se puntúa con la plantilla predeterminada. Las campañas no se crean solas. |
started_at no se entiende como fecha |
Se ignora. Si el nombre del archivo trae fecha, se usa esa; si no, la llamada queda fechada en el momento de la subida. |
started_at es anterior al año 2000 o está a más de un día en el futuro |
Se ignora, y en este caso tampoco se mira el nombre del archivo: la llamada queda fechada en el momento de la subida. |
direction tiene otro valor |
Se ignora. |
Respuesta
Sección titulada «Respuesta»201 Created cuando la llamada se ha creado:
{ "id": "k57e2xq9m4hc8w1t6b0z5y2e97c4n1ad", "status": "queued" }200 OK cuando ya existía una llamada con ese external_id. No se crea nada nuevo y el archivo recibido se descarta:
{ "id": "k57e2xq9m4hc8w1t6b0z5y2e97c4n1ad", "status": "exists" }| Campo | Descripción |
|---|---|
id |
El identificador de la llamada en Caller. Es el mismo que aparece en el webhook y en la dirección de la llamada en la app: https://app.caller.ee/calls/{id}. |
status |
queued si se acaba de crear y está en cola; exists si ya existía. |
Cualquier otro código es un error, con este cuerpo:
{ "error": { "code": "not_audio", "message": "The file is not an audio recording." } }La lista completa está en Errores.
Reintentar sin duplicar
Sección titulada «Reintentar sin duplicar»Las redes fallan. Si una petición se corta y no sabes si llegó, vuelve a enviarla con el mismo external_id:
- Si la primera no llegó, se crea la llamada y recibes
201. - Si sí había llegado, recibes
200con"status": "exists"y elidde la llamada que ya estaba.
En ninguno de los dos casos se cobra dos veces. Usa como external_id el identificador único que tu centralita da a cada llamada.
Sin external_id, cada petición crea una llamada nueva, aunque el archivo sea idéntico.
Formatos y tamaño
Sección titulada «Formatos y tamaño»- Formatos: MP3, WAV, OGG y M4A son los habituales. También se aceptan AAC, FLAC, Opus, WebM, WMA y AMR. Si tu centralita graba en uno de estos últimos, prueba con algunas llamadas antes de automatizar el envío.
- Tamaño máximo: la petición entera, con el archivo y los campos, tiene que pesar menos de 20 MB. Es un tope de la API, menor que el máximo por archivo de algunos planes: una grabación de entre 20 MB y el máximo de tu plan se puede subir desde la app, pero no por la API.
- Si te pasas: una petición de 20 MB o más se rechaza con
400 bad_request, el mismo código que un cuerpo mal formado. Comprueba el tamaño antes de enviar y comprime lo que no quepa: para voz, MP3 a 64 kbps en mono ocupa cerca de medio megabyte por minuto. - Tipo del archivo: si tu sistema lo envía como
application/octet-stream, Caller se guía por la extensión del nombre. Un archivo sin extensión de audio y sin tipoaudio/…se rechaza connot_audio.
Sin language, se usa el idioma de la campaña y, si no tiene, se detecta en cada llamada. Fíjalo cuando lo sepas con certeza: la detección puede confundir idiomas parecidos y necesita cerca de un minuto de conversación para acertar.
Códigos admitidos:
ca es en pt gl eu fr de it nl ar zh ru ja ko pl ro tr uk sv no da fi cs el hu he hi id th vi bg hr sk sr fa ur ms ta
Qué gasta cada envío
Sección titulada «Qué gasta cada envío»Cada llamada aceptada ocupa su tamaño en el almacenamiento y reserva minutos de audio. Su análisis va incluido en esos minutos.
La reserva se hace por el tamaño del archivo, a razón de un minuto por megabyte, y se ajusta a la duración real cuando termina la transcripción. Con MP3 la reserva se parece a la duración. Con WAV sin comprimir puede ser varias veces mayor, y una grabación puede rechazarse con no_minutes aunque su duración real sí cupiera. Si vas justo de minutos, envía audio comprimido.
Qué pasa después
Sección titulada «Qué pasa después»La llamada sigue el mismo camino que una subida a mano: se transcribe, se analiza y aparece en la app. Cuando queda analizada, Caller envía el resultado a tu webhook, si tienes uno.
No hay una petición para consultar el estado de una llamada. Si una grabación no se puede transcribir, lo verás en la app, en la vista Fallidas; no se envía ningún aviso.