Subir Llamadas para Análisis
📤 Subir Llamadas para Análisis
Sección titulada «📤 Subir Llamadas para Análisis»Cuando un proyecto de llamadas tiene el análisis automático activado, cada grabación se analiza en cuanto termina de transcribirse: no hace falta ninguna llamada adicional para «ejecutar el análisis».
Lo importante: el endpoint de subida de la API es el mismo que usa la aplicación web.
Arrastrar un archivo al panel de Conversaciones y enviarlo con POST a /api/files/ recorren
exactamente la misma ruta en el servidor, así que una grabación subida por un script se
transcribe, obtiene su CallRecord y se pone en cola para el análisis automático igual que una
subida a mano. No hay nada específico de la API que activar.
Todos los endpoints requieren una clave de API — consulta Autenticación.
El flujo de un vistazo
Sección titulada «El flujo de un vistazo»- Configura el proyecto una vez — activa el análisis automático y elige los tipos de análisis.
POST /api/files/— sube cada grabación, opcionalmente con sus metadatos de llamada.- Consulta
GET /calls/api/analysis-status/?project_id=…hasta quein_progressseafalse. - Lee los resultados desde la API de Informes de Llamadas.
Los pasos 2–4 son los únicos que repites en el día a día.
Todos los pasos necesitan el ID numérico del proyecto. Si no lo tienes, Proyectos explica cómo listar tus proyectos y dónde encontrar el ID en la aplicación web.
1. Configura el análisis automático
Sección titulada «1. Configura el análisis automático»El análisis automático se configura en el proyecto, mediante PATCH /api/projects/{id}/. También
puedes hacerlo desde el asistente de análisis de la aplicación web: los ajustes son los mismos.
Campos:
| Campo | Tipo | Descripción |
|---|---|---|
auto_analysis_enabled | bool | Interruptor principal. El análisis solo ocurre cuando es true. |
auto_analysis_types | lista | Qué análisis ejecutar por llamada. Uno o varios de calls_analysis_copc, calls_analysis_no_protocol, custom_questions. |
auto_analysis_questions | lista de textos | El conjunto de preguntas que se responde por llamada. Obligatorio cuando custom_questions está activo. |
evaluation_rubric | int | ID de la rúbrica con la que puntuar. Obligatorio cuando calls_analysis_copc está activo. |
auto_analysis_batch_size | int | Envía un lote cuando haya esta cantidad de llamadas esperando. Por defecto 20, rango permitido 1–20. |
auto_analysis_max_wait_seconds | int | Vacía un lote parcial cuando su llamada más antigua alcanza esta antigüedad. Por defecto 300. |
Validación — la petición se rechaza con 400 cuando la configuración produciría ejecuciones
inservibles:
- El
audio_typedel proyecto no escall— el análisis automático es solo para proyectos de llamadas. auto_analysis_typesestá vacío.- Se solicita
calls_analysis_copcsin unaevaluation_rubric. - Se solicita
custom_questionssin ningunaauto_analysis_questions. auto_analysis_batch_sizeestá fuera de1–20.
Ejemplo — activar el análisis por llamada sin protocolo más dos preguntas personalizadas:
curl -X PATCH https://app.uspeech.io/api/projects/312/ \ -H "Authorization: Api-Key $USPEECH_KEY" \ -H "Content-Type: application/json" \ -d '{ "auto_analysis_enabled": true, "auto_analysis_types": ["calls_analysis_no_protocol", "custom_questions"], "auto_analysis_questions": ["¿Se ofreció una devolución de llamada?", "¿Se mencionó el descuento?"], "auto_analysis_batch_size": 20, "auto_analysis_max_wait_seconds": 300 }'import osimport requests
BASE_URL = "https://app.uspeech.io"HEADERS = {"Authorization": f"Api-Key {os.environ['USPEECH_KEY']}"}project_id = 312
response = requests.patch( f"{BASE_URL}/api/projects/{project_id}/", headers=HEADERS, json={ "auto_analysis_enabled": True, "auto_analysis_types": ["calls_analysis_no_protocol", "custom_questions"], "auto_analysis_questions": [ "¿Se ofreció una devolución de llamada?", "¿Se mencionó el descuento?", ], "auto_analysis_batch_size": 20, "auto_analysis_max_wait_seconds": 300, }, timeout=30,)response.raise_for_status()print(response.json()["auto_analysis_types"])const BASE_URL = 'https://app.uspeech.io';const HEADERS = { Authorization: `Api-Key ${process.env.USPEECH_KEY}` };const projectId = 312;
const response = await fetch(`${BASE_URL}/api/projects/${projectId}/`, { method: 'PATCH', headers: { ...HEADERS, 'Content-Type': 'application/json' }, body: JSON.stringify({ auto_analysis_enabled: true, auto_analysis_types: ['calls_analysis_no_protocol', 'custom_questions'], auto_analysis_questions: [ '¿Se ofreció una devolución de llamada?', '¿Se mencionó el descuento?', ], auto_analysis_batch_size: 20, auto_analysis_max_wait_seconds: 300, }),});if (!response.ok) throw new Error(await response.text());console.log((await response.json()).auto_analysis_types);GET /api/projects/{id}/ devuelve los mismos campos, así que puedes consultar la configuración
actual antes de cambiarla.
2. Sube las grabaciones
Sección titulada «2. Sube las grabaciones»POST /api/files/ — idéntico a la subida de la API de Transcripción, y
acepta los metadatos de la llamada en la misma petición.
Content-Type: multipart/form-data
| Campo | Obligatorio | Descripción |
|---|---|---|
project | sí | El ID del proyecto. |
file | sí | La grabación (mp3, wav, m4a, flac, ogg, mp4, …), o una transcripción srt/vtt. |
file_type | sí | audio para grabaciones, o srt / vtt para transcripciones que ya tengas. |
call_datetime, agent_id, agent_name, direction, caller_number, campaign | no | Metadatos de la llamada — consulta Metadatos de Llamadas. |
Los metadatos son opcionales aquí, pero aportarlos es lo que permite agrupar el informe por agente, dirección, campaña y fecha. Para cargas masivas puedes omitirlos al subir y enviar en su lugar un manifiesto CSV: las filas se emparejan con los archivos por nombre, incluidos los archivos subidos más tarde.
curl -X POST https://app.uspeech.io/api/files/ \ -H "Authorization: Api-Key $USPEECH_KEY" \ -F "project=312" \ -F "file_type=audio" \ -F "file=@./call_001.mp3" \ -F "call_datetime=2026-06-15T09:12:00Z" \ -F "agent_id=A-06" \ -F "agent_name=Ana Diaz" \ -F "direction=inbound" \ -F "campaign=Retention"with open("call_001.mp3", "rb") as fh: response = requests.post( f"{BASE_URL}/api/files/", headers=HEADERS, data={ "project": project_id, "file_type": "audio", "call_datetime": "2026-06-15T09:12:00Z", "agent_id": "A-06", "agent_name": "Ana Diaz", "direction": "inbound", "campaign": "Retention", }, files={"file": ("call_001.mp3", fh, "audio/mpeg")}, timeout=300, )response.raise_for_status()file_id = response.json()["id"]print("subido", file_id)import { openAsBlob } from 'node:fs';
const form = new FormData();form.set('project', String(projectId));form.set('file_type', 'audio');form.set('call_datetime', '2026-06-15T09:12:00Z');form.set('agent_id', 'A-06');form.set('agent_name', 'Ana Diaz');form.set('direction', 'inbound');form.set('campaign', 'Retention');form.set('file', await openAsBlob('./call_001.mp3'), 'call_001.mp3');
const response = await fetch(`${BASE_URL}/api/files/`, { method: 'POST', headers: HEADERS, // no fijes Content-Type — FormData define el boundary body: form,});if (!response.ok) throw new Error(await response.text());const fileId = (await response.json()).id;console.log('subido', fileId);⚠️ Importante: 201 Created significa que la subida se aceptó y se puso en cola. Tanto la
transcripción como el análisis ocurren de forma asíncrona — consulta la sección de sondeo más
abajo.
Volver a subir una grabación
Sección titulada «Volver a subir una grabación»Un archivo subido con un nombre que el proyecto ya tenía se trata como la misma llamada: el nuevo registro sustituye al anterior en vez de añadir un segundo. Esto hace que los reintentos sean seguros: si una subida masiva falla a medias, repetirla no duplicará llamadas en el informe.
3. Cómo funcionan los lotes
Sección titulada «3. Cómo funcionan los lotes»Las llamadas no se analizan de una en una, sino en lotes pequeños, y por eso los resultados aparecen en grupos en lugar de uno a uno.
Un lote de un tipo de análisis se envía cuando se cumple cualquiera de estas condiciones:
- Hay
auto_analysis_batch_sizellamadas esperando — el lote sale de inmediato. - La llamada más antigua en espera alcanza
auto_analysis_max_wait_seconds— un barrido que se ejecuta cada 60 segundos vacía el lote parcial.
Así, un lote parcial nunca se queda atascado: con los valores por defecto, una llamada suelta
subida al final del día se analiza en unos cinco minutos. Si subes en tandas grandes, baja
auto_analysis_max_wait_seconds solo si necesitas resultados antes — los lotes más pequeños
cuestan más por llamada.
4. Sigue el progreso
Sección titulada «4. Sigue el progreso»Hay dos niveles de visibilidad:
Por archivo — GET /api/files/{id}/ informa del status de transcripción (transcribing →
transcribed | failed) y de un campo auto_analysis_error que expone el motivo por el que se
omitió el análisis automático de ese archivo, por ejemplo quota_exceeded cuando la suscripción
se quedó sin minutos.
Por proyecto — GET /calls/api/analysis-status/?project_id={id} es el endpoint que el propio
panel consulta:
{ "analysis_status": { "calls_analysis_no_protocol": { "completed": 57, "queued": 3 } }, "analyzed_calls": 57, "duplicate_uploads": 2, "in_progress": true, "reclustering": false, "last_analyzed_at": "2026-07-29T02:50:11.127124+00:00"}Consúltalo hasta que in_progress sea false y entonces descarga el informe. Consulta
Informes de Llamadas para la referencia completa de campos.
while :; do resp=$(curl -sS "https://app.uspeech.io/calls/api/analysis-status/?project_id=312" \ -H "Authorization: Api-Key $USPEECH_KEY") in_progress=$(echo "$resp" | jq -r .in_progress) echo "$(date -u +%H:%M:%S) analizadas=$(echo "$resp" | jq -r .analyzed_calls) in_progress=$in_progress" [ "$in_progress" = "false" ] && break sleep 30doneimport time
while True: status = requests.get( f"{BASE_URL}/calls/api/analysis-status/", headers=HEADERS, params={"project_id": project_id}, timeout=30, ).json() print(f"analizadas={status['analyzed_calls']} in_progress={status['in_progress']}") if not status["in_progress"]: break time.sleep(30)const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
while (true) { const status = await fetch( `${BASE_URL}/calls/api/analysis-status/?project_id=${projectId}`, { headers: HEADERS }, ).then((r) => r.json()); console.log(`analizadas=${status.analyzed_calls} in_progress=${status.in_progress}`); if (!status.in_progress) break; await sleep(30_000);}💡 Consejo: consulta cada 30 segundos o más despacio. La transcripción tarda aproximadamente 1/8 de la duración del audio, y el análisis espera a que se llene un lote, así que sondear más rápido no aporta información nueva.
5. Lee los resultados
Sección titulada «5. Lee los resultados»Cuando in_progress sea false, las llamadas analizadas están disponibles a través de la
API de Informes de Llamadas:
GET /calls/api/report/?project_id={id}— el informe de calidad agregadoGET /calls/api/calls/?project_id={id}— el explorador de llamadasGET /calls/api/custom-questions/?project_id={id}— respuestas por llamada a tus preguntas personalizadasGET /calls/api/report/pdf/?project_id={id}— la exportación en PDF
Ejemplo de principio a fin
Sección titulada «Ejemplo de principio a fin»Configura el proyecto, sube una carpeta de grabaciones, adjunta sus metadatos de llamada, espera al análisis y descarga el informe.
El paso de los metadatos importa: sin ellos las llamadas se analizan igualmente, pero el informe no tiene nada por lo que agruparlas — sin agente, sin dirección, sin campaña, y con la hora de subida haciendo de hora de la llamada. Hay dos formas de aportarlos, y puedes combinarlas:
- Por archivo al subir, como campos adicionales del formulario en
POST /api/files/— ideal cuando tu sistema ya conoce los datos de cada llamada al subirla. Es lo que muestra el paso 2 más arriba. - Por proyecto, como manifiesto CSV en
POST /api/projects/{id}/call-manifest/— ideal para cargas masivas y para rellenar datos a posteriori. Las filas se emparejan con los archivos por nombre, y las que aún no coinciden con ninguno se guardan y se aplican a los archivos subidos más tarde, así que el manifiesto puede enviarse antes o después de las grabaciones.
El ejemplo de abajo usa el manifiesto, porque encaja con subir una carpeta entera de una vez. Consulta Metadatos de Llamadas para la referencia completa de campos, los formatos de fecha aceptados y cómo corregir los metadatos después.
#!/usr/bin/env bashset -euo pipefail
BASE_URL="https://app.uspeech.io"AUTH="Authorization: Api-Key ${USPEECH_KEY}"project_id=312
# 1. Activar el análisis automáticocurl -sS -X PATCH "${BASE_URL}/api/projects/${project_id}/" \ -H "$AUTH" -H "Content-Type: application/json" \ -d '{"auto_analysis_enabled": true, "auto_analysis_types": ["calls_analysis_no_protocol"]}' \ > /dev/null
# 2. Subir todas las grabaciones de ./calls/for path in ./calls/*.mp3; do curl -sS -X POST "${BASE_URL}/api/files/" \ -H "$AUTH" \ -F "project=${project_id}" \ -F "file_type=audio" \ -F "file=@${path}" \ | jq -r '"subido \(.original_filename) -> \(.id)"'done
# 3. Adjuntar los metadatos de llamada de todo el lote.# llamadas_junio.csv: filename,call_datetime,agent_id,agent_name,direction,campaigncurl -sS -X POST "${BASE_URL}/api/projects/${project_id}/call-manifest/" \ -H "$AUTH" -F "file=@./llamadas_junio.csv" \ | jq -r '"\(.matched) asociadas, \(.unmatched | length) esperando su archivo"'
# 4. Esperar a que el análisis terminewhile :; do resp=$(curl -sS "${BASE_URL}/calls/api/analysis-status/?project_id=${project_id}" -H "$AUTH") [ "$(echo "$resp" | jq -r .in_progress)" = "false" ] && break sleep 30done
# 5. Descargar el informecurl -sS "${BASE_URL}/calls/api/report/?project_id=${project_id}" -H "$AUTH" > report.jsonecho "informe escrito en report.json"import osimport timefrom pathlib import Path
import requests
BASE_URL = "https://app.uspeech.io"HEADERS = {"Authorization": f"Api-Key {os.environ['USPEECH_KEY']}"}project_id = 312
# 1. Activar el análisis automáticorequests.patch( f"{BASE_URL}/api/projects/{project_id}/", headers=HEADERS, json={ "auto_analysis_enabled": True, "auto_analysis_types": ["calls_analysis_no_protocol"], }, timeout=30,).raise_for_status()
# 2. Subir todas las grabaciones de ./calls/for path in sorted(Path("calls").glob("*.mp3")): with path.open("rb") as fh: response = requests.post( f"{BASE_URL}/api/files/", headers=HEADERS, data={"project": project_id, "file_type": "audio"}, files={"file": (path.name, fh, "audio/mpeg")}, timeout=300, ) response.raise_for_status() print(f"subido {path.name} -> {response.json()['id']}")
# 3. Adjuntar los metadatos de llamada de todo el lote.# llamadas_junio.csv: filename,call_datetime,agent_id,agent_name,direction,campaignwith open("llamadas_junio.csv", "rb") as fh: manifest = requests.post( f"{BASE_URL}/api/projects/{project_id}/call-manifest/", headers=HEADERS, files={"file": ("llamadas_junio.csv", fh, "text/csv")}, timeout=120, )manifest.raise_for_status()result = manifest.json()print(f"{result['matched']} asociadas, {len(result['unmatched'])} esperando su archivo")
# 4. Esperar a que el análisis terminewhile True: status = requests.get( f"{BASE_URL}/calls/api/analysis-status/", headers=HEADERS, params={"project_id": project_id}, timeout=30, ).json() if not status["in_progress"]: break time.sleep(30)
# 5. Descargar el informereport = requests.get( f"{BASE_URL}/calls/api/report/", headers=HEADERS, params={"project_id": project_id}, timeout=60,).json()print(f"{report['headline']['total_calls']} llamadas en el informe")import { readdir } from 'node:fs/promises';import { openAsBlob } from 'node:fs';
const BASE_URL = 'https://app.uspeech.io';const HEADERS = { Authorization: `Api-Key ${process.env.USPEECH_KEY}` };const projectId = 312;const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 1. Activar el análisis automáticoawait fetch(`${BASE_URL}/api/projects/${projectId}/`, { method: 'PATCH', headers: { ...HEADERS, 'Content-Type': 'application/json' }, body: JSON.stringify({ auto_analysis_enabled: true, auto_analysis_types: ['calls_analysis_no_protocol'], }),});
// 2. Subir todas las grabaciones de ./calls/const names = (await readdir('calls')).filter((name) => name.endsWith('.mp3'));for (const name of names) { const form = new FormData(); form.set('project', String(projectId)); form.set('file_type', 'audio'); form.set('file', await openAsBlob(`calls/${name}`), name);
const response = await fetch(`${BASE_URL}/api/files/`, { method: 'POST', headers: HEADERS, body: form, }); if (!response.ok) throw new Error(`${name}: ${await response.text()}`); console.log(`subido ${name} -> ${(await response.json()).id}`);}
// 3. Adjuntar los metadatos de llamada de todo el lote.// llamadas_junio.csv: filename,call_datetime,agent_id,agent_name,direction,campaignconst manifestForm = new FormData();manifestForm.set('file', await openAsBlob('./llamadas_junio.csv'), 'llamadas_junio.csv');
const manifest = await fetch(`${BASE_URL}/api/projects/${projectId}/call-manifest/`, { method: 'POST', headers: HEADERS, body: manifestForm,});if (!manifest.ok) throw new Error(await manifest.text());const { matched, unmatched } = await manifest.json();console.log(`${matched} asociadas, ${unmatched.length} esperando su archivo`);
// 4. Esperar a que el análisis terminewhile (true) { const status = await fetch( `${BASE_URL}/calls/api/analysis-status/?project_id=${projectId}`, { headers: HEADERS }, ).then((r) => r.json()); if (!status.in_progress) break; await sleep(30_000);}
// 5. Descargar el informeconst report = await fetch( `${BASE_URL}/calls/api/report/?project_id=${projectId}`, { headers: HEADERS },).then((r) => r.json());console.log(`${report.headline.total_calls} llamadas en el informe`);Solución de problemas
Sección titulada «Solución de problemas»| Síntoma | Causa probable |
|---|---|
El archivo llega a transcribed pero nunca se analiza nada | auto_analysis_enabled es false, o auto_analysis_types está vacío. Comprueba GET /api/projects/{id}/. |
auto_analysis_error es quota_exceeded | La suscripción se quedó sin minutos. El análisis se omite, no se reintenta automáticamente. |
400 en la subida con un campo de metadatos en el cuerpo | El proyecto no es de llamadas, o file_type no es audio/srt/vtt. |
400 al activar el análisis automático | Falta la rúbrica para COPC, faltan preguntas para custom_questions, o el tamaño de lote está fuera de 1–20. |
| El recuento de llamadas del informe es menor que los archivos subidos | Los nombres reutilizados sustituyen a la llamada anterior. duplicate_uploads en la respuesta de estado del análisis los cuenta. |
Consulta Códigos de estado y errores para la lista completa de códigos de respuesta.