API
Адрес: https://console.gapi.uz. Каждый запрос с заголовком Authorization: Bearer <токен>. Токены создаются в кабинете во вкладке «API-токены» и привязаны к проекту.
Ответы в 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 МиБ |
settings | JSON с настройками на этот запрос поверх настроек проекта, необязательно |
vocabulary | JSON-массив терминов на этот запрос вместо словаря проекта, необязательно |
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.
Записи не обязательно загружать: если она уже лежит по ссылке, пришлите ссылку, и мы скачаем её сами. Так удобнее интеграциям с телефонией, где запись и так лежит в хранилище.
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: список
{"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 принимает те же ключи, что вкладка «Настройки» кабинета; неуказанные берутся из проекта.
{
"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": [...]} | Сохранить новую версию и включить её; пустой массив убирает словарь |
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.
Каждое сохранение — новая пронумерованная версия, и активной становится последняя, так что неудачная правка это шаг назад, а не потеря. История версий видна в кабинете.
Словарь на один запрос
Если термины нужны только для этой записи — обрабатываете чужой каталог, проверяете список перед тем как сохранить, — пришлите их вместе с файлом:
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/usage возвращает баланс в g и дневную цель пополнения:
{"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:
{"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_token | 401 | Нет заголовка Authorization или токен неверный либо отозван |
account_blocked | 403 | Аккаунт заблокирован; напишите нам |
file_required | 400 | В multipart нет поля file |
invalid_multipart, invalid_json | 400 | Тело не разбирается |
invalid_url, fetch_failed | 400, 502 | Ссылка не http(s), не разбирается или файл по ней не скачался |
invalid_webhook_url | 400 | webhook_url не http(s) |
invalid_call_direction, invalid_max_speakers | 400 | Значение вне допустимого: направление это inbound, outbound или пусто; спикеров до 4 |
unknown_setting | 400 | В settings ключ, которого нет; подсказка называет его и перечисляет существующие |
upload_failed | 400 | Тело запроса пустое или оборвалось на середине |
invalid_speaker_name | 400 | Имя голоса вне 1–120 символов |
missing_job_id | 400 | enroll без job_id |
speaker_index_out_of_range | 400 | В результате нет спикера с таким номером; подсказка говорит, сколько их |
invalid_request | 400 | Значение не той формы, которую ждёт ручка |
method_not_allowed | 405 | Тот же адрес принимает другой метод; он назван в заголовке Allow |
no_voiceprint | 422 | У этого спикера нет отпечатка: нужна опция «Узнавать по голосу» и хотя бы 3 секунды речи |
insufficient_speech | 422 | Для отпечатка размечено меньше 3 секунд речи |
terms_required | 400 | PUT /v1/vocabulary без terms |
term_too_long, term_too_many_words, too_many_terms | 400 | Термин длиннее 80 символов, больше 4 слов, или терминов больше 20 |
audio_unreadable | 422 | Файл не декодируется |
audio_too_long | 422 | Запись длиннее 4 часов |
too_long_for_sync | 422 | /v1/transcribe для записи длиннее 5 минут; используйте /v1/jobs |
file_too_large | 413 | Больше 200 МиБ. Если размер виден в заголовке Content-Length, ответ приходит сразу, не дожидаясь конца загрузки |
Подождать и повторить.
| Код | HTTP | Когда |
|---|---|---|
rate_limited | 429 | Слишком много запросов; см. Лимиты |
insufficient_balance | 429 | Баланса в g не хватает на эту запись; подсказка говорит, сколько есть и когда пополнение |
too_many_active_jobs | 429 | У проекта уже столько задач в очереди и в работе, сколько разрешено |
result_not_ready | 409 | Задача ещё идёт; опрашивайте GET /v1/jobs/{id} или подпишитесь на события |
no_runner_for_diarization, no_runner_for_options | 503 | Нет свободной машины с нужной моделью; повторите позже |
storage_full | 507 | На нашей стороне кончилось место; мы это видим |
Уже не вернуть.
| Код | HTTP | Когда |
|---|---|---|
job_not_found | 404 | Нет такой задачи в этом проекте — её никогда не было, она удалена или id не тот |
speaker_not_found | 404 | Нет такого голоса в этом проекте |
job_result_not_found | 404 | enroll ссылается на задачу, которая не готова, не из этого проекта или уже удалена |
no_sample | 404 | Сохранённой записи этого голоса нет |
not_found | 404 | Такого адреса в API нет |
result_expired | 410 | Результат старше 30 дней и удалён |
audio_expired | 410 | Сама запись уже не хранится: образец голоса не проиграть |
Отдельно internal с 500: это уже наша поломка, а не ваша. Такой ответ стоит повторить, и если он повторяется — напишите нам, мы видим его в своих логах.
Неверный id — не UUID, пустая переменная из шаблона — это 404, а не 500: такой задачи просто нет.