Skip to content

API ​

Адрес: https://console.gapi.uz. Каждый запрос с заголовком Authorization: Bearer <токен>. Токены создаются в кабинете во вкладке «API-токены» и привязаны к проекту.

Ответы в JSON. Ошибка выглядит так:

json
{"error": "insufficient_balance", "hint": "balance 3.20 g (0.00 g held by jobs in flight), this job needs 4.71 g; the beta balance is topped up to 240 g at midnight Asia/Tashkent"}

Задачи ​

POST /v1/jobs: загрузить запись ​

multipart/form-data:

ПолеЧто это
fileАудио: mp3, wav, ogg/opus, m4a, flac, webm. До 200 МиБ
settingsJSON с настройками на этот запрос поверх настроек проекта, необязательно
vocabularyJSON-массив терминов на этот запрос вместо словаря проекта, необязательно
webhook_urlКуда прислать уведомление о готовности, необязательно

Ответ 202: {"id", "state": "queued", "class": "batch", "estimated_seconds", "created_at", "result_url"}. result_url абсолютный — его можно сохранить и позвать как есть. class это batch у обычной загрузки и interactive у POST /v1/transcribe.

Список форматов не исчерпывающий: принимается всё, что читает ffmpeg, — например сырой поток AAC. Если файл не декодируется, приходит 422 audio_unreadable.

Записи не обязательно загружать: если она уже лежит по ссылке, пришлите ссылку, и мы скачаем её сами. Так удобнее интеграциям с телефонией, где запись и так лежит в хранилище.

bash
curl -X POST https://console.gapi.uz/v1/jobs \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com/call.mp3", "settings": {"pii": false}}'

Третий способ — прислать сам файл телом запроса, без multipart, с именем в заголовке X-File-Name. Настройки тогда берутся из проекта.

Те же три формы принимает и POST /v1/transcribe.

GET /v1/jobs/{id}: состояние ​

state: queued, running, done, failed, cancelled. Пока идёт, есть progress_seconds. У неудачи есть error.

GET /v1/jobs/{id}/events: события ​

text/event-stream. Это не только индикатор прогресса: реплики приходят сюда по мере того, как они готовы, так что текст можно показывать во время разговора, не дожидаясь конца записи.

СобытиеЧто в нём
segmentsМассив готовых реплик по порядку, в том же виде, что в результате
stateЗадача сменила состояние: в data та же задача, что отдаёт GET /v1/jobs/{id}
doneЗадача кончилась; поток закрывается. В data — {"state", "job"}, а у готовой ещё и result_url
event: segments
data: [{"index": 0, "start": 0.42, "end": 3.9, "speaker": 1, "text": "Assalomu alaykum…"}]

Подключившийся поздно получит сначала всё, что уже готово, и только потом новое, — так что пропустить начало нельзя.

Последняя порция перед done присылает реплики ещё раз, уже в окончательном виде: пока запись идёт, пунктуация в них предварительная. Поэтому складывайте реплики по index, заменяя пришедшую раньше, — тогда к концу потока у вас ровно тот же текст, что и в результате.

Каждые двадцать секунд в поток уходит строка-комментарий : keepalive, чтобы прокси не закрыл соединение. Если поток оборвался, ничего не потеряно: подпишитесь снова или возьмите результат целиком.

GET /v1/jobs/{id}/result: результат ​

Описан в разделе Формат ответа. Результат хранится 30 дней.

GET /v1/jobs: список ​

json
{"jobs": [{"id": "41f79b78-…", "state": "done", "class": "batch", "estimated_seconds": 283,
           "audio_seconds": 282.7, "created_at": "…", "finished_at": "…"}]}

Последние 50 задач проекта, новые сверху; ?limit= поднимает до 200. Число больше максимума не отбрасывается, а зажимается: limit=500 вернёт те же 200. Постраничного обхода нет: это взгляд на недавнее, а не выгрузка архива. Удалённые задачи в списке не показываются.

DELETE /v1/jobs/{id}: отменить или удалить ​

Один запрос делает то из двух, что сейчас имеет смысл.

Задача в очереди или в работе — отменяется. Состояние становится cancelled, списания не будет, приходит вебхук job.cancelled. В ответе сама задача. Отмена это остановка работы, а не забвение: задача остаётся в списке.

Задача уже кончилась — удаляется. Ответ {"id", "state", "deleted": true}, и с этого момента её нет: ни в списке, ни по GET /v1/jobs/{id}, ни в результате, ни в потоке событий; повторный DELETE отвечает 404 job_not_found. Чтобы и отменить, и удалить, позовите DELETE дважды.

Удаление скрывает задачу от вас, но не стирает её у нас: списание за неё остаётся в истории счёта, а запись и расшифровка живут свой обычный срок хранения. Иначе счёт было бы нечем объяснить.

POST /v1/transcribe: коротко и сразу ​

Те же поля, что у POST /v1/jobs, для записей до 5 минут. Ответ 200 это сам результат, идентификатор задачи в заголовке X-Job-Id. Если за 3 минуты результат не готов, приходит 202 с job_id: дальше опрашивайте GET /v1/jobs/{id}.

Настройки на запрос ​

Поле settings принимает те же ключи, что вкладка «Настройки» кабинета; неуказанные берутся из проекта.

json
{
  "formatting": true,
  "itn": true,
  "word_timestamps": false,
  "stereo_channels_as_speakers": true,
  "diarization": true,
  "max_speakers": 2,
  "gender": true,
  "speaker_roles": true,
  "call_direction": "inbound",
  "speaker_identification": true,
  "pii": false
}

Сюда же кладётся webhook_url, если удобнее держать его рядом с остальными настройками; отдельное поле запроса, если оно есть, важнее.

Ключи проверяются: неизвестный отклоняет загрузку с 400 unknown_setting, и подсказка называет его. Так diarisation вместо diarization — это ошибка сразу, а не обычная расшифровка, за которую списали как за обычную.

Что делает каждый ключ: Опции.

Словарь проекта ​

Те же термины, что и на вкладке «Словарь» в кабинете: названия компаний, продуктов, имена. По умолчанию список принадлежит проекту: задача берёт копию активной версии в момент создания, поэтому правка словаря не переписывает уже отправленную работу. Отдельная запись может прийти и со своим списком — ниже.

ЗапросЧто делает
GET /v1/vocabularyАктивный словарь: terms, version, updated_at, min_terms, max_terms
PUT /v1/vocabulary {"terms": [...]}Сохранить новую версию и включить её; пустой массив убирает словарь
bash
curl -X PUT https://console.gapi.uz/v1/vocabulary \
     -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
     -d '{"terms": ["MetaSell", "Bitrix24", "amoCRM", "Toshkent"]}'

От 1 до 20 терминов, до 4 слов и 80 символов в термине. Повторы и пустые строки отбрасываются молча, порядок сохраняется; повтор считается без учёта регистра, так что «metasell» и «MetaSell» это один термин, и остаётся первый. Чем короче список, тем точнее модель попадает в нужное слово, так что перечисляйте только то, что действительно звучит в ваших записях. Ошибки: term_too_long, term_too_many_words, too_many_terms.

Каждое сохранение — новая пронумерованная версия, и активной становится последняя, так что неудачная правка это шаг назад, а не потеря. История версий видна в кабинете.

Словарь на один запрос ​

Если термины нужны только для этой записи — обрабатываете чужой каталог, проверяете список перед тем как сохранить, — пришлите их вместе с файлом:

bash
curl -X POST https://console.gapi.uz/v1/jobs \
  -H "Authorization: Bearer $TOKEN" \
  -F file=@call.mp3 \
  -F 'vocabulary=["MetaSell", "Bitrix24", "amoCRM"]'

Список заменяет словарь проекта для этой задачи и только для неё: активная версия не меняется, следующая запись снова пойдёт со словарём проекта. Пустой массив значит «эту запись считать без словаря вовсе». Правила и коды ошибок те же, что у PUT, и неверный термин отклоняет загрузку до того, как появится задача.

В JSON-форме загрузки поле называется так же: "vocabulary": [...]. При отправке файла голым телом запроса словарь берётся из проекта. POST /v1/transcribe принимает поле наравне с POST /v1/jobs.

Что применилось, видно в результате: vocabulary_terms — сколько терминов взяла задача, vocabulary_dropped — true, если после чистки не осталось ни одного.

Голоса проекта ​

ЗапросЧто делает
GET /v1/speakersСписок голосов: id, number, name (пустое у безымянных), voiceprints, а также kind, seen_count, last_seen_at, expires_at, has_sample
POST /v1/speakers {"name"}Создать голос без отпечатков
PATCH /v1/speakers/{id} {"name"}Дать имя или переименовать
DELETE /v1/speakers/{id}Удалить голос со всеми отпечатками
POST /v1/speakers/{id}/enrollДобавить отпечаток из готовой записи: {"job_id", "speaker_index"} или {"job_id", "segment_indexes": [...]}
DELETE /v1/speakers/{id}/voiceprintsУдалить все отпечатки, оставить имя
GET /v1/speakers/{id}/sampleДо двенадцати секунд аудио, где этот голос слышно (audio/wav; обычно короче — берётся самая длинная реплика). 404 no_sample, если сохранённой записи этого голоса нет, 410 audio_expired, если сама запись уже не хранится
DELETE /v1/speakers?kind=candidateУдалить разом все голоса одного вида: candidate, recurring или named

GET /v1/speakers и DELETE /v1/speakers?kind= принимают ?tenant=, чтобы работать с голосами одного бизнеса; без него они работают с голосами самого проекта.

Подробно: Голоса и спикеры.

Тенанты ​

Для интегратора, который обслуживает несколько бизнесов из одного проекта. Укажите бизнес каждой записи в settings: "tenant": "biz-86233275", 1–64 символа из A–Z a–z 0–9 . _ : -, в запросе или в настройках проекта. Задание его запоминает: его несут GET /v1/jobs/{id}, список заданий, результат (поле tenant верхнего уровня) и вебхук. Задание без тенанта работает и выглядит в точности как раньше.

ЗапросЧто делает
GET /v1/tenantsВсе тенанты проекта: jobs, audio_seconds и billed_g (что списано, включая задания, удалённые позже), staff, staff_enrolled, staff_voices, voices, last_job_at
GET /v1/usage?tenant=…&from=…&to=…Сколько списано за одного тенанта по дням (Asia/Tashkent): jobs, audio_seconds, billed_g и итоги. from и to в виде YYYY-MM-DD, по умолчанию сегодня, не больше 366 дней
GET /v1/jobs?tenant=…Задания тенанта
DELETE /v1/tenants/{tenant}Удалить голоса тенанта: линии сотрудников с их отпечатками, другие голоса и отпечатки внутри его результатов
DELETE /v1/tenants/{tenant}?jobs=trueУдалить и его записи: незаконченные задания отменяются, расшифровки и аудио удаляются (аудио, которое прислал и другой тенант, остаётся для него), а каждое задание остаётся только строкой счёта, скрытой от всех запросов. Ответ считает, что удалено; "ok": false с audio_failed значит, что запись пока не удалось удалить, повторите вызов. Повторное удаление отвечает нулями

Первое применение тенантов — голоса: голоса двух тенантов никогда не сравниваются (Голоса и спикеры).

Линии сотрудников тенанта ​

Линии сотрудников каждого тенанта, узнаваемые только среди своих. Линия это один external_id, на котором работают до четырёх человек. Как это работает: Только сотрудники.

ЗапросЧто делает
GET /v1/tenants/{tenant}/staffЛинии одного тенанта: external_id, role, name, voice_id, status (pending, enrolled, inconsistent), voices (каждый voice 1–4 с calls — в скольких звонках окна услышан — и last_heard_at), window_calls, printing, calls_seen (звонки, назвавшие линию), calls_matched (из них звонки, где её голос узнан), enrolled_at, last_matched_at (когда один из её голосов слышали последний раз, в любом звонке тенанта)
GET /v1/tenants/{tenant}/staff/{external_id}Одна линия
PUT /v1/tenants/{tenant}/staff/{external_id} {"role", "name"}Добавить линию или сменить роль или имя; 201 для новой. Голоса появятся из звонков, где она названа
POST /v1/tenants/{tenant}/staff/{external_id}/enroll {"job_id", "speaker_index"}Указать, какой спикер готового звонка того же тенанта — сотрудник линии, если текстовая модель выбрала другую сторону: 202, сторона печатается из хранимой записи и считается как любой звонок линии
DELETE /v1/tenants/{tenant}/staff/{external_id}Удалить линию со всеми голосами, их отпечатками и запомненными звонками; ответ считает, что удалено. Повторное удаление отвечает нулями
DELETE /v1/tenants/{tenant}/staff/{external_id}/voices/{voice}Удалить одного человека линии (голос 1–4)

Ни один запрос не возвращает голосовой отпечаток.

Использование ​

GET /v1/usage возвращает баланс в g и дневную цель пополнения:

json
{"unit": "g", "balance_g": 203.63, "in_flight_g": 4.5, "available_g": 199.13, "topup_target_g": 240,
 "next_topup": "00:00 Asia/Tashkent", "day": "2026-09-09", "used_seconds": 3425.6, "remaining_seconds": 11947.8}

GET /v1/billing отдаёт баланс и историю пополнений и списаний. Как считается стоимость: Баланс и оплата.

Вебхуки ​

Если у проекта задан webhook_url или он передан в запросе, по завершении задачи на этот адрес приходит POST с JSON:

json
{"event": "job.done", "id": "41f79b78-…", "state": "done", "class": "batch", "estimated_seconds": 283,
 "audio_seconds": 282.7, "error": null, "created_at": "…", "finished_at": "…",
 "result_url": "https://console.gapi.uz/v1/jobs/41f79b78-…/result"}

event это job. и состояние: job.done, job.failed, job.cancelled, — по нему письмо разбирается одним switch. audio_seconds это длительность записи, за которую списано; у неудачной задачи его нет.

Вебхук приходит на любой конец задачи, а не только на удачный: state бывает done, failed и cancelled, так что отменённая или упавшая задача тоже закрывает ожидание, и таймаут на вашей стороне нужен только на случай, если мы не дозвонимся.

Первый запрос уходит сразу, как только задача завершилась: он не ждёт фонового обхода — завершение задачи само будит доставку.

Ответьте любым 2xx, на ответ есть десять секунд. Каждая доставка открывает своё соединение и закрывает его: мы не держим коннект на вашей стороне.

При ошибке доставка повторяется до 5 раз, и пауза отсчитывается не от прошлой попытки, а от конца задачи: примерно 30 с, 2 мин, 4.5 мин и 8 мин после того, как она завершилась. Повторы разбираются раз в минуту, поэтому эти числа приблизительные — в отличие от первого запроса.

Удалённая задача вебхука не присылает.

Лимиты ​

ЧтоСколько
Размер файла200 МиБ (209 715 200 байт)
Длительность записидо 4 часов
Синхронный запрос /v1/transcribeзаписи до 5 минут
Загрузок в минуту на токен60
Запросов на чтение в минуту на токен3000
Управляющих запросов в минуту на токен600
Хранение результата30 дней
Баланспополняется до 240 g каждую полночь по Ташкенту
Проектов на аккаунт, токенов на проект3, 10
Голосов на проект, отпечатков на голос500, 10
Терминов в словареот 1 до 20

Отправка записи и вопрос о ней стоят разного, поэтому и лимиты разные: загрузка занимает машину с моделью, а GET /v1/jobs/{id} это одна строка в базе. Опрашивать состояние можно свободно — 3000 запросов в минуту это тридцать задач по два опроса в секунду. Но лучше GET /v1/jobs/{id}/events или вебхук: тогда опрашивать не нужно вовсе.

Ведра три, и считаются они раздельно. В загрузки попадают POST /v1/jobs и POST /v1/transcribe. В управляющие — всё, что меняет проект, но не занимает машину: голоса и словарь. Остальное это чтение.

В ответе 429 есть заголовок Retry-After с числом секунд: столько осталось до того, как освободится место в окне. Ждать дольше не нужно, меньше — бесполезно.

Числа в этой таблице — значения по умолчанию, а не константы: цель пополнения, размер файла, длительность и одновременные задачи на проект меняются для аккаунта по договорённости.

При превышении приходит 429: rate_limited, insufficient_balance или too_many_active_jobs.

Коды ошибок ​

Код лежит в поле error, рядом человекочитаемый hint. Стоит различать три группы: чинить запрос, ждать и повторять, смириться.

Не примут запрос как есть.

КодHTTPКогда
missing_token, invalid_token401Нет заголовка Authorization или токен неверный либо отозван
account_blocked403Аккаунт заблокирован; напишите нам
file_required400В multipart нет поля file
invalid_multipart, invalid_json400Тело не разбирается
invalid_url, fetch_failed400, 502Ссылка не http(s), не разбирается или файл по ней не скачался
invalid_webhook_url400webhook_url не http(s)
invalid_call_direction, invalid_max_speakers400Значение вне допустимого: направление это inbound, outbound или пусто; спикеров до 4
unknown_setting400В settings ключ, которого нет; подсказка называет его и перечисляет существующие
upload_failed400Тело запроса пустое или оборвалось на середине
invalid_speaker_name400Имя голоса вне 1–120 символов
tenant_required400known_speakers или "enroll": "known_only" без tenant
invalid_tenant, invalid_external_id, invalid_role, invalid_known_speakers, duplicate_external_id, invalid_enroll400Ключ тенанта или сотрудников не той формы: тенант 1–64, идентификатор 1–128 символов из A–Z a–z 0–9 . _ : - (в идентификаторе ещё @); role это manager, operator или supervisor; не больше четырёх известных спикеров, каждый один раз; enroll это auto, known_only или off, и не auto вместе с known_speakers
missing_job_id400enroll без job_id
speaker_index_out_of_range400В результате нет спикера с таким номером; подсказка говорит, сколько их
invalid_request400Значение не той формы, которую ждёт ручка
method_not_allowed405Тот же адрес принимает другой метод; он назван в заголовке Allow
no_voiceprint422У этого спикера нет отпечатка: нужна опция «Узнавать по голосу» и хотя бы 3 секунды речи
insufficient_speech422Для отпечатка размечено меньше 3 секунд речи
tenant_mismatch422Линию поправляют звонком другого тенанта
invalid_voice400Голоса линии нумеруются от 1 до 4
too_many_staff422У тенанта не больше 500 линий сотрудников
terms_required400PUT /v1/vocabulary без terms
term_too_long, term_too_many_words, too_many_terms400Термин длиннее 80 символов, больше 4 слов, или терминов больше 20
audio_unreadable422Файл не декодируется
audio_too_long422Запись длиннее 4 часов
too_long_for_sync422/v1/transcribe для записи длиннее 5 минут; используйте /v1/jobs
file_too_large413Больше 200 МиБ. Если размер виден в заголовке Content-Length, ответ приходит сразу, не дожидаясь конца загрузки

Подождать и повторить.

КодHTTPКогда
rate_limited429Слишком много запросов; см. Лимиты
insufficient_balance429Баланса в g не хватает на эту запись; подсказка говорит, сколько есть и когда пополнение
too_many_active_jobs429У проекта уже столько задач в очереди и в работе, сколько разрешено
result_not_ready409Задача ещё идёт; опрашивайте GET /v1/jobs/{id} или подпишитесь на события
no_runner_for_diarization, no_runner_for_options503Нет свободной машины с нужной моделью; повторите позже
storage_full507На нашей стороне кончилось место; мы это видим

Уже не вернуть.

КодHTTPКогда
job_not_found404Нет такой задачи в этом проекте — её никогда не было, она удалена или id не тот
speaker_not_found404Нет такого голоса в этом проекте
staff_not_found404У этого тенанта нет линии с таким external_id
job_result_not_found404enroll ссылается на задачу, которая не готова, не из этого проекта или уже удалена
no_sample404Сохранённой записи этого голоса нет
not_found404Такого адреса в API нет
result_expired410Результат старше 30 дней и удалён
audio_expired410Сама запись уже не хранится: образец голоса не проиграть

Отдельно internal с 500: это уже наша поломка, а не ваша. Такой ответ стоит повторить, и если он повторяется — напишите нам, мы видим его в своих логах.

Неверный id — не UUID, пустая переменная из шаблона — это 404, а не 500: такой задачи просто нет.