Ir al contenido

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.

EndpointCuerpoÚsalo cuando…
POST /recordsJSONTienes un fileUrl desde el que Aelo puede descargar (o una transcripción de chat)
POST /records/uploadmultipart/form-dataTienes 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.

RutaLímitePor qué
POST /records (fileUrl)100 MB, o 25 MB si el origen no envía Content-LengthAelo 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 MBAelo 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 MBUn 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.

Terminal window
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" requiere fileUrl y rechaza textContent.
  • type: "chat" requiere textContent (el texto de la transcripción) y rechaza fileUrl.
Terminal window
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.

CampoRequeridoNota
projectIdLos espacios sobrantes se recortan antes de la búsqueda; un valor solo de espacios se rechaza
typeJSON: sí · multipart: nocall o chat; en multipart el valor por defecto es call y nunca acepta chat
fileUrlJSON, solo llamadasNo se acepta con type: "chat"
textContentJSON, solo chatsNo se acepta con type: "call"
externalIdNoTu propio id para este registro — ver Idempotencia abajo
occurredAtNoISO-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
agentIdNoFormato libre: los ids de CRM suelen ser strings numéricos, los de telefonía son ids de operador. Ver Advertencias
agentName, participantName, participantPhone, agentPhoneNo
durationSecondsNoOmítelo si no lo tienes — ver Advertencias. 0–86400 (24 h); fuera de ese rango la solicitud se rechaza
callDirectionNoinbound u outbound
languageNo2–8 caracteres
crmEntityTypeNodeal, lead, contact o company
crmEntityId, crmEntityName, crmActivityNameNo

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.

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.

Una subida exitosa (200 o 201) igual puede traer warnings — el registro se creó, pero parte de la analítica no lo verá:

CódigoCuándoMensaje
missing_agent_idSin agentId«No agentId supplied: this record is excluded from agent ratings (they filter on agent_id IS NOT NULL).»
missing_crm_entitySin el par crmEntityType/crmEntityId«No crmEntityType/crmEntityId pair: this record joins no call series, so it appears in no funnel or deal outcome.»
missing_durationtype: "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.

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.