Subir llamadas y chats
Hay dos formas de subir una grabación de llamada (o una transcripción de chat) y crear un registro, y cuál usar depende de dónde vive ya tu audio.
| Endpoint | Cuerpo | Úsalo cuando… |
|---|---|---|
POST /records | JSON | Tienes un fileUrl desde el que Aelo puede descargar (o una transcripción de chat) |
POST /records/upload | multipart/form-data | Tienes el audio en tus manos y no tienes dónde alojarlo |
Ambos requieren el scope records:write.
Ambos también requieren que el proyecto de destino esté active. Un proyecto
paused o archived responde project_not_accepting_records (409) en lugar
de aceptar el registro — el administrador de la organización debe
reactivarlo antes de que ese projectId pueda volver a aceptar registros.
Límites de tamaño
Section titled “Límites de tamaño”| Ruta | Límite | Por qué |
|---|---|---|
POST /records (fileUrl) | 100 MB, o 25 MB si el origen no envía Content-Length | Aelo transmite la descarga directamente al almacenamiento; un origen que no declara su longitud no puede transmitirse y se retiene en memoria |
POST /records (cuerpo JSON / textContent) | 10 MB | Aelo lee todo el cuerpo JSON en memoria antes de poder validarlo, así que el tope coincide con el límite multipart de chat de abajo |
POST /records/upload (multipart) | 25 MB | Un cuerpo multipart debe almacenarse por completo en memoria antes de que Aelo pueda leerlo, así que el tope es mucho más bajo |
Si tu archivo pesa más de 25 MB, alójalo en algún lugar al que Aelo pueda
acceder por HTTPS y llama a POST /records con fileUrl en lugar de subir
los bytes directamente.
Subir por URL
Section titled “Subir por URL”curl -X POST https://api.aelo.cloud/api/public/v1/records \ -H "Authorization: Bearer aelo_sk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "projectId": "your-project-id", "type": "call", "externalId": "crm-call-12345", "fileUrl": "https://your-storage.example.com/call.mp3", "agentId": "42", "agentName": "Ivan Petrov", "durationSeconds": 184, "callDirection": "outbound", "crmEntityType": "deal", "crmEntityId": "9001" }'fileUrl debe ser https:// y resolver a una dirección pública — Aelo
rechaza las URL que apuntan a redes privadas o link-local. Aelo espera hasta
30 segundos por la descarga; una fuente más lenta o inalcanzable se traduce
en fetch_failed (problema en la propia URL) o resolution_unavailable
(problema en el resolutor de Aelo, transitorio — reintenta).
La solicitud debe declarar Content-Length. Un cuerpo con codificación
chunked (Transfer-Encoding: chunked) se rechaza con 400 antes de que Aelo
intente analizarlo — la misma regla que la subida multipart de abajo. Esto
aplica a toda la solicitud, no solo a type: "chat": un cuerpo con fileUrl
es minúsculo y nunca se acerca al límite de 10 MB, pero la comprobación se
ejecuta antes de que Aelo sepa de qué rama se trata.
type es "call" o "chat":
type: "call"requierefileUrly rechazatextContent.type: "chat"requieretextContent(el texto de la transcripción) y rechazafileUrl.
Subir un archivo directamente
Section titled “Subir un archivo directamente”curl -X POST https://api.aelo.cloud/api/public/v1/records/upload \ -H "Authorization: Bearer aelo_sk_YOUR_KEY" \ -F "file=@call.mp3" \ -F "projectId=your-project-id" \ -F "externalId=crm-call-12345" \ -F "agentId=42" \ -F "agentName=Ivan Petrov" \ -F "callDirection=outbound"El audio va en la parte llamada file; el resto de las partes son campos de
metadatos, con los mismos nombres que en el cuerpo JSON (los valores llegan
como strings y se convierten donde hace falta — durationSeconds, por
ejemplo). Por esta vía solo se pueden subir llamadas: type por defecto es
"call", y enviar type=chat se rechaza — envía una transcripción de chat a
POST /records con textContent. Si tu formulario incluye por error una
segunda parte también llamada file, solo se lee la primera; el resto se
ignora en silencio.
La solicitud debe declarar Content-Length. Un cuerpo con codificación
chunked (Transfer-Encoding: chunked) se rechaza con 400 antes de que Aelo
intente analizarlo. Cualquier cliente normal lo establece automáticamente —
curl -F, fetch con un cuerpo File/Blob, el helper multipart de
cualquier SDK — así que esto solo afecta a una solicitud construida a mano
sobre un ReadableStream.
Campos requeridos y opcionales
Section titled “Campos requeridos y opcionales”| Campo | Requerido | Nota |
|---|---|---|
projectId | Sí | Los espacios sobrantes se recortan antes de la búsqueda; un valor solo de espacios se rechaza |
type | JSON: sí · multipart: no | call o chat; en multipart el valor por defecto es call y nunca acepta chat |
fileUrl | JSON, solo llamadas | No se acepta con type: "chat" |
textContent | JSON, solo chats | No se acepta con type: "call" |
externalId | No | Tu propio id para este registro — ver Idempotencia abajo |
occurredAt | No | ISO-8601. Hora de negocio de la llamada — ver la nota sobre paneles más abajo. Se rechaza antes de 2000-01-01 o a más de 24 h en el futuro |
agentId | No | Formato libre: los ids de CRM suelen ser strings numéricos, los de telefonía son ids de operador. Ver Advertencias |
agentName, participantName, participantPhone, agentPhone | No | |
durationSeconds | No | Omítelo si no lo tienes — ver Advertencias. 0–86400 (24 h); fuera de ese rango la solicitud se rechaza |
callDirection | No | inbound u outbound |
language | No | 2–8 caracteres |
crmEntityType | No | deal, lead, contact o company |
crmEntityId, crmEntityName, crmActivityName | No |
Cualquier campo que esta API no reconozca se rechaza con invalid_request en
lugar de descartarse en silencio — una errata como projectID falla de forma
explícita en lugar de perder datos sin que lo notes.
Un campo opcional en blanco significa “no enviado”. Si un campo del
formulario llega vacío o solo con espacios (-F "agentId=", o una variable de
shell sin definir interpolada en un formulario), Aelo lo trata como ausente
en lugar de como una cadena vacía — incluyendo externalId y agentId. Con
un campo requerido ocurre lo contrario: un valor en blanco es un 400, no
se interpreta como “no enviado”. Esta distinción solo aplica a los cuerpos
multipart; un "" en JSON es un valor escrito a propósito y se procesa tal
cual.
Idempotencia
Section titled “Idempotencia”externalId es tu propio id para un registro — un id de actividad de CRM, un
id de llamada de telefonía, lo que ya use tu sistema. Envía el mismo
externalId de nuevo y Aelo devuelve el registro existente en lugar de crear
un duplicado (200 en vez de 201).
La unicidad es por organización, no por proyecto. Si envías el mismo
externalId bajo dos projectId distintos dentro de la misma organización,
la segunda llamada no crea un segundo registro — devuelve el registro del
primer proyecto. Si tu integración reutiliza ids de llamada entre
proyectos distintos, mantenlos únicos de tu lado o cuenta con que la segunda
subida resuelva al registro del primer proyecto.
Advertencias
Section titled “Advertencias”Una subida exitosa (200 o 201) igual puede traer warnings — el registro
se creó, pero parte de la analítica no lo verá:
| Código | Cuándo | Mensaje |
|---|---|---|
missing_agent_id | Sin agentId | «No agentId supplied: this record is excluded from agent ratings (they filter on agent_id IS NOT NULL).» |
missing_crm_entity | Sin el par crmEntityType/crmEntityId | «No crmEntityType/crmEntityId pair: this record joins no call series, so it appears in no funnel or deal outcome.» |
missing_duration | type: "call" sin durationSeconds | «No durationSeconds: leaderboard totals and short-call detection ignore this record.» |
warnings solo aparece en las dos respuestas de subida — no forma parte del
formato de registro que devuelven los endpoints GET. Un durationSeconds
en blanco en un formulario multipart se trata igual que omitirlo: Aelo nunca
lo guarda como 0, y la advertencia missing_duration se dispara igual.
Los paneles agrupan por hora de subida
Section titled “Los paneles agrupan por hora de subida”Aelo guarda occurredAt en el registro, pero los paneles e informes de v1
agrupan las llamadas por el momento en que Aelo las recibió (createdAt), no
por occurredAt. Si subes llamadas históricas con fecha retroactiva y un
occurredAt de semanas o meses atrás, aparecerán en las cifras de hoy, no en
el período en que ocurrieron realmente. Por eso la importación histórica
masiva no es compatible en v1 — esta API está pensada para subir llamadas
y chats a medida que ocurren.
Relacionado
Section titled “Relacionado”- Resumen de la API pública — autenticación, los endpoints de lectura y la tabla completa de errores