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": {"emotion": 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,
  "emotion": false,
  "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_droppedtrue, если после чистки не осталось ни одного.

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

ЗапросЧто делает
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/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 символов
missing_job_id400enroll без job_id
speaker_index_out_of_range400В результате нет спикера с таким номером; подсказка говорит, сколько их
invalid_request400Значение не той формы, которую ждёт ручка
method_not_allowed405Тот же адрес принимает другой метод; он назван в заголовке Allow
no_voiceprint422У этого спикера нет отпечатка: нужна опция «Узнавать по голосу» и хотя бы 3 секунды речи
insufficient_speech422Для отпечатка размечено меньше 3 секунд речи
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Нет такого голоса в этом проекте
job_result_not_found404enroll ссылается на задачу, которая не готова, не из этого проекта или уже удалена
no_sample404Сохранённой записи этого голоса нет
not_found404Такого адреса в API нет
result_expired410Результат старше 30 дней и удалён
audio_expired410Сама запись уже не хранится: образец голоса не проиграть

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

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