Перейти к содержимому

Загрузка звонков и чатов

Есть два способа загрузить запись звонка (или текст чата) и создать из неё запись в Aelo — какой использовать, зависит от того, где уже находится ваше аудио.

ЭндпоинтТелоИспользуйте, когда…
POST /recordsJSONУ вас есть fileUrl, по которому Aelo может скачать файл (или текст чата)
POST /records/uploadmultipart/form-dataАудио уже у вас на руках, и разместить его негде

Оба эндпоинта требуют скоуп records:write.

Оба также требуют, чтобы целевой проект был в статусе active. Проект в статусе paused или archived отвечает project_not_accepting_records (409) вместо приёма записи — прежде чем projectId снова сможет принимать записи, администратор организации должен реактивировать проект.

ПутьЛимитПочему
POST /records (fileUrl)100 МБ, либо 25 МБ, если источник не сообщает Content-LengthAelo скачивает файл потоком напрямую в хранилище; источник, не сообщающий длину, потоком передать нельзя — он держится в памяти
POST /records (JSON-тело / textContent)10 МБВсё JSON-тело читается в память целиком до валидации, поэтому потолок совпадает с multipart-лимитом для чата ниже
POST /records/upload (multipart)25 МБMultipart-тело нужно полностью буферизовать в памяти перед чтением, поэтому потолок намного ниже

Если ваш файл больше 25 МБ, разместите его там, куда Aelo может обратиться по HTTPS, и вызовите POST /records с fileUrl вместо передачи самих байтов.

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 обязан быть https:// и указывать на публичный адрес — Aelo отклоняет URL, ведущие в приватные или link-local сети. Aelo ждёт скачивание до 30 секунд; более медленный или недоступный источник вернётся как fetch_failed (проблема в самом URL) или resolution_unavailable (проблема на стороне резолвера Aelo, временная — повторите запрос).

Запрос обязан объявить Content-Length. Тело с chunked-кодировкой (Transfer-Encoding: chunked) отклоняется с 400 ещё до попытки разбора — то же правило, что и для multipart-загрузки ниже. Это касается всего запроса, а не только type: "chat": тело с fileUrl крошечное и никогда не приближается к лимиту в 10 МБ, но проверка выполняется раньше, чем Aelo узнаёт, какая это ветка.

type — это "call" или "chat":

  • type: "call" требует fileUrl и отклоняет textContent.
  • type: "chat" требует textContent (текст транскрипта) и отклоняет fileUrl.

Загрузка файла напрямую

Section titled “Загрузка файла напрямую”
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"

Аудио передаётся в части с именем file; все остальные части — поля метаданных под теми же именами, что и в JSON-теле (значения приходят строками и приводятся к нужному типу там, где нужно — например, durationSeconds). Так можно загружать только звонки: type по умолчанию равен "call", а type=chat отклоняется — отправьте текст чата на POST /records с textContent. Если в форме случайно оказалась вторая часть с именем file, прочитана будет только первая — остальные молча игнорируются.

Запрос обязан объявить Content-Length. Тело с chunked-кодировкой (Transfer-Encoding: chunked) отклоняется с 400 ещё до попытки разбора. Любой обычный клиент выставляет этот заголовок автоматически — curl -F, fetch с телом File/Blob, multipart-хелпер любого SDK, — так что это затрагивает только запрос, вручную собранный поверх ReadableStream.

Обязательные и необязательные поля

Section titled “Обязательные и необязательные поля”
ПолеОбязательноПримечание
projectIdДаПробелы по краям обрезаются перед поиском; значение только из пробелов отклоняется
typeJSON: да · multipart: нетcall или chat; в multipart по умолчанию call и chat не принимается
fileUrlJSON, только звонкиНе принимается для type: "chat"
textContentJSON, только чатыНе принимается для type: "call"
externalIdНетВаш собственный id записи — см. Идемпотентность ниже
occurredAtНетISO-8601. Время звонка по бизнес-часам — см. примечание про дашборды ниже. Отвергается раньше 2000-01-01 и дальше чем на 24 ч в будущее
agentIdНетПроизвольный формат: id из CRM обычно числовая строка, id из телефонии — id оператора. См. Предупреждения
agentName, participantName, participantPhone, agentPhoneНет
durationSecondsНетНе передавайте, если значения нет — см. Предупреждения. 0–86400 (24 ч); вне диапазона запрос отвергается
callDirectionНетinbound или outbound
languageНет2–8 символов
crmEntityTypeНетdeal, lead, contact или company
crmEntityId, crmEntityName, crmActivityNameНет

Любое нераспознанное поле этот API отклоняет с invalid_request, а не отбрасывает молча — опечатка вроде projectID даёт явную ошибку, а не тихую потерю данных.

Пустое необязательное поле означает «не отправлено». Если поле формы приходит пустым или состоящим только из пробелов (-F "agentId=", или неустановленная переменная окружения, подставленная в форму), Aelo трактует это как отсутствие значения, а не как пустую строку — включая externalId и agentId. С обязательным полем всё иначе: пустое значение — это 400, оно не считается «не отправленным». Это различие касается только multipart-тел; JSON "" — значение, введённое намеренно, и обрабатывается как есть.

externalId — ваш собственный id записи: id активности из CRM, id звонка из телефонии — что уже используется в вашей системе. Отправьте тот же externalId повторно, и Aelo вернёт существующую запись вместо создания дубликата (200 вместо 201).

Уникальность действует в рамках организации, а не проекта. Если вы отправите один и тот же externalId под двумя разными projectId в одной организации, второй вызов не создаст вторую запись — он вернёт запись первого проекта. Если ваша интеграция переиспользует id звонков между разными проектами, либо делайте их уникальными на своей стороне, либо закладывайтесь на то, что вторая загрузка «схлопнется» в запись первого проекта.

Успешная загрузка (200 или 201) всё равно может содержать warnings — запись создана, но часть аналитики её не увидит:

КодКогдаСообщение
missing_agent_idНет agentId«No agentId supplied: this record is excluded from agent ratings (they filter on agent_id IS NOT NULL).»
missing_crm_entityНет пары crmEntityType/crmEntityId«No crmEntityType/crmEntityId pair: this record joins no call series, so it appears in no funnel or deal outcome.»
missing_durationtype: "call" без durationSeconds«No durationSeconds: leaderboard totals and short-call detection ignore this record.»

warnings появляется только в ответах обоих эндпоинтов загрузки — этого поля нет в формате записи, который возвращают GET-эндпоинты. Пустое значение durationSeconds в multipart-форме трактуется так же, как его отсутствие: Aelo никогда не сохранит его как 0, и предупреждение missing_duration всё равно сработает.

Дашборды группируют по времени загрузки

Section titled “Дашборды группируют по времени загрузки”

Aelo сохраняет occurredAt в записи, но дашборды и отчёты в v1 группируют звонки по времени, когда Aelo их получила (createdAt), а не по occurredAt. Если вы задним числом загружаете исторические звонки с occurredAt недельной или месячной давности, они попадут в сегодняшние цифры, а не в тот период, когда произошли на самом деле. Поэтому массовый исторический импорт в v1 не поддерживается — этот API предназначен для загрузки звонков и чатов по мере их появления.