API iVox
Редакция 2026-09-10
Тринадцать инструментов из вашего кода. Одиннадцать отвечают прямо сейчас: озвучка, многоголосый диалог, пакетная озвучка, регенерация фрагмента, звуковые эффекты, музыка, изоляция голоса, смена голоса, дубляж, расшифровка и субтитры по готовому тексту — теми же движками и по тем же тарифам, что в интерфейсе. Плюс чтение каталога голосов.

Быстрый старт
Поставить озвучку и забрать файл — целиком. Подставьте ключ и голос.
curl -X POST https://ivoxstudio.ru/v1/tts \ -H "Authorization: Bearer $IVOX_API_KEY" \ -H "Idempotency-Key: order-1782" \ -H "Content-Type: application/json" \ -d '{ "text": "Привет! Это подкаст «Голос технологий».", "voice_id": "1", "format": "mp3_44100_128" }' # 202 Accepted # { "id": "9f2b41c8-3d5e-4a17-8c60-2e7b1d904f33", "status": "queued", ... } curl https://ivoxstudio.ru/v1/jobs/9f2b41c8-3d5e-4a17-8c60-2e7b1d904f33 \ -H "Authorization: Bearer $IVOX_API_KEY"
В примере статус опрашивается циклом — так короче и понятнее. В работающей интеграции ждать нужно вебхуком: лимит чтения — 120 запросов в минуту на ключ, а не на задачу, и два десятка задач, опрашиваемых раз в секунду, упрутся в 429. Если без опроса никак — держите паузу и пользуйтесь ?wait=30.
Как это устроено
Одна механика на все инструменты
Меняется тело запроса, а не способ работы.
Шаг 01
Ключ
Выпускаете ключ в этой вкладке и кладёте его в переменные окружения своего сервиса. Ключ показывается один раз — мы храним только его отпечаток.
Шаг 02
Запрос
Отправляете задачу: текст и голос для озвучки, файл для расшифровки, описание для эффекта. В ответ сразу приходит идентификатор задачи, ждать не нужно.
Шаг 03
Ожидание
Мы присылаем вебхук на ваш адрес, когда задача готова. Если вебхук негде принять — опрашиваете статус сами, но это запасной путь.
Шаг 04
Результат
Забираете файл по ссылке из ответа. Ссылка подписана и живёт сутки — за это время её нужно скачать себе.
Адрес и авторизация
https://ivoxstudio.ru/v1
Публичный API отделён от внутреннего: свой префикс /v1, свои гарантии совместимости и своя авторизация — ключом, а не сессией. Отдельного поддомена у него пока нет.
Authorization: Bearer ivox_live_bRZsdUoXP6Y7VQsASM3TWmNJ7wkr Content-Type: application/json
Ключ — только на сервере. В коде страницы или мобильного приложения он виден любому, кто откроет отладчик, и расход пойдёт с вашего баланса. Для браузера держите свой бэкенд-прокси. По той же причине у API нет CORS и ключ нельзя передать параметром адреса.
Отдельного поддомена api.ivoxstudio.ru у публичного API пока нет: /v1 живёт на основном домене. Когда поддомен появится, этот адрес продолжит работать — убирать его без нового мажора мы не станем.
Порядок вызова
Кто кому стучится
Ответ на запрос — это идентификатор задачи, а не файл. Готовность приходит вебхуком на ваш адрес; опрос статуса — запасной путь, когда вебхук принять негде.
Состояние
API работает: ключи выдаются, 13 инструментов отвечают
Публичный API живёт на /v1 основного домена — своего поддомена api.ivoxstudio.ru пока нет. Выпустите ключ ниже, поставьте задачу, заберите файл по подписанной ссылке. Тестовый ключ (ivox_test_…) ходит по тем же адресам бесплатно: задача сразу приходит готовой, с коротким образцом файла по настоящей ссылке.
Уже есть
- 33 метода, 13 инструментовОзвучка, диалог, пакет, регенерация, эффекты, музыка, изоляция, смена голоса, дубляж, расшифровка, субтитры — все на единой модели задачи. Видео и говорящий аватар описаны здесь же, но до задания тарифа отвечают 403 tool_unavailable.
- Ключи, скоупы и тестовый режимКлюч показывается один раз, права выдаются явно. Тестовый ключ ничего не списывает и не пишет в библиотеку, но его задачи настоящие: видны в /jobs, повторяются по Idempotency-Key и присылают вебхуки.
- Вебхуки, лимитер, идемпотентностьТри терминальных события с подписью и повторами, лимит по ключу с заголовками X-RateLimit-*, безопасный ретрай по Idempotency-Key.
- Спецификация OpenAPI 3.1Справочник — /developers/openapi, файл — /openapi.yaml. Описывает ровно то, что отвечает: клиент по ней генерируется, коллекция в Postman импортируется и работает.
Ещё нет
- Домена api.ivoxstudio.ru — API отвечает на /v1 основного домена
- Заголовка Ivox-Version: дата-версий и версии, закреплённой за ключом, нет
- IP-allowlist и даты истечения у ключа: при выпуске задаются только имя, права и режим
- Событий job.queued, job.processing, balance.low, key.revoked — только три терминальных
- Писем о сбоящем адресе вебхука: состояние видно в GET /v1/webhooks и в истории доставок
- Лимита на число одновременно активных задач
- Отмены задачи в processing — там 409, отменить можно только очередную
- Тарифа на видео и на говорящего аватара: пока он не задан, оба метода отвечают 403 tool_unavailable, а не работают даром
- Оценки (/estimate) для всего, кроме озвучки
- Голосов на клонирование и дизайн, словаря, проектов, каталога звуков и агентов
Инструментов в API 13 из восемнадцати разделов сайта: клонирования и дизайна голосов, словаря, проектов, каталога звуков и голосовых агентов здесь нет — они остались только в интерфейсе. Правило одно: сначала код, потом документация.
Справочник по спецификации OpenAPI 3.1 — все методы, поля и ответы. Скачать openapi.yaml — для генерации клиента и импорта в Postman.
Что дальше
Чего в API ещё нет и что для этого нужно
Порядок здесь не обязательный: каждый пункт самостоятельный и ничего из работающего не блокирует. Правило одно — сначала код, потом документация. Строка в спеке про метод, которого нет, снова превратит документацию в обещание, а сгенерированный по ней клиент — в 404.
Соглашения
Правила, которые действуют везде
- Только HTTPSТело — JSON в UTF-8, кроме загрузки файлов (multipart/form-data); JSON-метод с другим Content-Type — 415 unsupported_media_type. Ссылки на исходные файлы и адреса вебхуков — тоже только https:// на публичный адрес. Отдельного 426 на голый http мы не отдаём.
- Размер JSON-телаДо 1 МиБ, и предел считается по мере чтения, а не только по Content-Length. Предел на файл единым числом не задан: он свой у каждого инструмента — 50 МиБ у субтитров, 2 ГиБ у дубляжа.
- Поля — snake_caseВнутренний API отдаёт camelCase, публичный слой переводит. Публичный контракт не наследует привычки фронта.
- Идентификаторы непрозрачныОбещанных префиксов типа (key_…, whk_…) на деле нет: боевые задачи, подписки и ключи носят UUID, а задачи тестового ключа начинаются с job_test_…. Префикс есть у события (evt_…), у секрета подписки (whsec_…), у самого ключа (ivox_live_… / ivox_test_…) и у запроса (req_…). Не парсить ни те, ни другие.
- Время и длительностиRFC 3339 в UTC: 2026-09-09T11:20:31Z. Длительности — в секундах, поле кончается на _sec. result.duration_sec — всегда целое (округлено), у эффекта тоже: дробным он бывает только в запросе (0.5–30). Дробными в ответах приходят таймкоды в массивах — segments[] расшифровки, lines[] диалога, cues[] субтитров.
- Расход — целые символыЕдиница одна, дробных списаний нет.
- null и отсутствие поля равнозначныПустая строка — не null.
Чего нет намеренно
- Нет OAuth и работы от лица чужого пользователяКлюч — это один наш аккаунт, расход идёт с его баланса. Схема «моё приложение озвучивает своим пользователям» решается на стороне клиента, а не делегированием доступа.
- Нет ключей в параметрах адресаТолько заголовок: query попадает в логи и в Referer.
- Нет cookie-сессий и CSRF-логикиПубличный API не для браузера напрямую. Ключ в клиентском JS — это утечка ключа; для браузера нужен свой backend-прокси.
- CORS выключенЗаголовок Access-Control-Allow-Origin не отдаётся — по той же причине.
Ключи
Ключ показывается один раз — мы храним только его отпечаток. При выпуске задаются только имя, права и режим (боевой или тестовый). Ключ живёт до отзыва.
Ключ выпускает любой вошедший пользователь — без заявки и одобрения. Войдите, и выпуск откроется на этой же вкладке.
Войти и выпустить ключПрава ключа
Выдаются явно: скоупа «всё сразу» в модели нет
Запрос вне выданных прав — 403. Минимальная интеграция «озвучить текст и забрать файл» — это tts:read tts:write: статус озвучки читается по tts:read, а подписанная ссылка на файл права не требует.
tts:readСтатус задачи озвучки, фрагменты записи, оценка стоимостиtts:writeОзвучка, диалог, пакет, регенерация фрагментаvoices:readКаталог и личные голосаvoices:writeПока не открывает ничего: клонирования и дизайна голосов в /v1 нетaudio:readПерсонажи и чтение задач звуковых инструментовaudio:writeЭффекты, музыка, изоляция, смена голоса, дубляж, расшифровка, субтитры, видео, аватарыaccount:readБаланс, расход, список подписок и история доставокaccount:writeСоздание и правка вебхуков, ротация секрета, повтор доставкиТестовый ключ ходит по тем же маршрутам, но ничего не списывает, не пишет в библиотеку и не зовёт движок. Задача при этом настоящая: сразу приходит в succeeded с livemode: false, все ссылки в result ведут на короткий образец файла, её видно в GET /jobs и GET /jobs/{id}, Idempotency-Key работает как у боевого, а подписки тестового режима получают job.succeeded. Так интеграцию можно отладить бесплатно, включая вебхуки. Ни дата-версии, ни списка разрешённых адресов, ни даты истечения задать нечем: этих полей в форме выпуска нет. Ротация — новый ключ выпускается рядом, старый отзывается руками, когда сервисы переехали.
Жизненный цикл задачи
Одна модель на все инструменты
Почти всё, что мы делаем, идёт к внешнему движку и занимает секунды или минуты. Вы пишете один поллер и один обработчик вебхука.
queuedПринята, ждёт очереди. Списание уже произошло по оценке — у всех инструментов, включая музыку. У файла по ссылке списана минута, разница досчитывается перед обработкой.промежуточныйprocessingОтдана провайдеру и считается.промежуточныйsucceededГотова, в result лежит ссылка на файл.терминальныйfailedНе получилась. Списанное возвращено полностью.терминальныйcanceledОтменена вами, пока стояла в очереди, — у любого инструмента, озвучки тоже. Списанное возвращено целиком. Начатую задачу отменить нельзя — POST /jobs/{id}/cancel ответит 409 job_not_cancelable.терминальныйОбъект задачи на /jobs/{id}
{
"id": "9f2b41c8-3d5e-4a17-8c60-2e7b1d904f33",
"object": "job",
"type": "tts",
"status": "queued",
"livemode": true,
"created_at": "2026-09-09T11:20:31Z",
"updated_at": "2026-09-09T11:20:31Z",
"started_at": null,
"finished_at": null,
"request": { "text": "…", "voice_id": "1", "model": "ivox-expressive" },
"usage": { "chars_estimated": 128, "chars_charged": 128, "chars_refunded": 0 },
"result": null,
"error": null
}Это форма общего адреса задач. У озвучки свой, более узкий объект: POST /tts и GET /tts/{job_id} отдают те же поля кроме started_at, finished_at и request. Нужны сроки и тело запроса — читайте задачу озвучки на GET /jobs/{id}: туда она попадает тем же идентификатором.
Работа с результатом
Ссылка подписана и не является местом постоянного хранения — файл нужно скачать себе. Перевыпустить её можно в любой момент: каждый ответ GET /jobs/{id} отдаёт свежую подпись. Отмена работает только в queued — у любого инструмента, озвучки тоже — и возвращает списанное целиком; возврат виден в usage.chars_refunded, а подпискам уходит job.canceled. В processing отмены нет — придёт 409 job_not_cancelable: генерация у провайдера уже оплачена, и обрывать её значит платить за неё дважды.
Списки и постраничный обход
Курсорная пагинация, а не смещение
На живых данных offset врёт: пока вы идёте по страницам, в начало списка добавляются новые задачи, и часть записей вы увидите дважды, а часть пропустите.
Параметры списка
Обход страниц
# Первая страница curl "https://ivoxstudio.ru/v1/jobs?type=tts&status=succeeded&limit=50" \ -H "Authorization: Bearer $IVOX_API_KEY" # { "object": "list", "data": [...], "has_more": true, # "next_cursor": "MjAyNi0wOS0wOVQxMToyMDozMVp8OWYyYjQxYzg" } # Следующая — строкой next_cursor как есть, с теми же фильтрами. # Не id последнего элемента: собранный руками курсор — 422 invalid_cursor. curl -G "https://ivoxstudio.ru/v1/jobs" \ -H "Authorization: Bearer $IVOX_API_KEY" \ --data-urlencode "type=tts" \ --data-urlencode "status=succeeded" \ --data-urlencode "limit=50" \ --data-urlencode "starting_after=MjAyNi0wOS0wOVQxMToyMDozMVp8OWYyYjQxYzg" # has_more: false — страниц больше нет. Курсор ведёт только вперёд.
Идемпотентность
Защита от двойного списания
Обязательна по смыслу для всего, что стоит денег: формально мы ключ не требуем, но интеграция без него на повторах будет платить дважды — и это будет её счёт.
Как это работает
Срока годности у ключа нет: он живёт столько же, сколько строка задачи, и повтор через неделю вернёт ту же задачу. Кода request_in_flight не существует. Тестовый ключ ведёт себя так же, но ключи тестового и боевого режима живут отдельно и друг с другом не конфликтуют. Полей metadata и project_id в API нет — своё сопоставление с заказом держите на Idempotency-Key.
Повтор без второго списания
# Первый запрос: задача создана, символы списаны. curl -X POST https://ivoxstudio.ru/v1/tts \ -H "Authorization: Bearer $IVOX_API_KEY" \ -H "Idempotency-Key: order-1782" \ -H "Content-Type: application/json" \ -d '{"text":"Привет","voice_id":"1"}' # 202 Accepted { "id": "9f2b41c8-3d5e-…", "status": "queued" } # Соединение оборвалось, повторяем с ТЕМ ЖЕ ключом: # 200 OK тот же 9f2b41c8-3d5e-…, второй задачи нет, второго списания нет. # Тот же ключ, но другое тело: # 409 { "error": { "code": "idempotency_key_reuse" } } # Тот же ключ на другом инструменте (например, /sfx после /tts) — тоже 409. # Ключ длиннее 255 символов — 422. # Срока годности у ключа нет: он живёт столько же, сколько строка задачи, # и повтор через неделю вернёт ту же задачу. Кода request_in_flight не существует. # Тестовый ключ ведёт себя так же, но ключи тестового и боевого режима не пересекаются.
Инструменты
Что именно умеет сервис: параметры, пределы, стоимость и рабочие примеры по каждому инструменту. Источник — код публичного контура, а не контракт: здесь нет ни одного поля, которого нет в API.
Озвучка
Озвучка текста
Синтез речи из текста. Основной инструмент: тем же движком и теми же голосами, что раздел «Озвучка текста» в интерфейсе. Единственный, у которого есть свой адрес статуса — остальные читаются на GET /v1/jobs/{id}.
Как звать
Пределы
Параметры
Что принимает тело запроса
textstringТекст озвучки, 1–5000 символов. Длиннее — 422: резать на части должен клиент, молча обрезать чужой текст мы не будем. Текст из одних пробелов — тоже 422.обязателенvoice_idstringИдентификатор голоса из GET /voices: числовая строка — каталожный голос, буквенная — личный (клон или голос из дизайна). Голоса по умолчанию нет намеренно — иначе смена нашего дефолта тихо меняет звук у всех интеграций.обязателенmodelstringivox-expressive (единственная понимает теги эмоций), ivox-multilingual (стабильная), ivox-fast (быстрая). Незнакомое значение — не ошибка: подставим поддерживаемую и вернём фактическую в result.model.по умолчанию ivox-expressiveformatstringПринимается ровно одно значение — mp3_44100_128. Очередь синтезирует в формате из настроек сервера и другой не отдаст, поэтому «wav» или «ogg» — это 422 unsupported_format, а не молчаливая подмена.по умолчанию mp3_44100_128settings.speedint 0–100Темп речи.по умолчанию 65settings.stabilityint 0–100Стабильность интонации: выше — ровнее и предсказуемее, ниже — выразительнее. Единственная настройка, которую использует ivox-expressive; остальные три уезжают провайдеру, но эта модель их не применяет.по умолчанию 50settings.similarityint 0–100Схожесть с оригиналом голоса.по умолчанию 75settings.styleint 0–100Выраженность стиля.по умолчанию 10normalizeboolРазворачивать ли числа и сокращения в слова на стороне провайдера. На списание не влияет — платите за набранные символы.по умолчанию falseПример
Запрос и ответ целиком
Тела приведены как есть — с заголовками, кодом ответа и полями результата.
Запрос
POST /v1/tts
Authorization: Bearer ivox_live_...
Idempotency-Key: order-1782
Content-Type: application/json
{
"text": "Привет! Это подкаст «Голос технологий».",
"voice_id": "1",
"model": "ivox-expressive",
"format": "mp3_44100_128",
"settings": { "speed": 65, "stability": 50, "similarity": 75, "style": 10 },
"normalize": false
}Ответ
202 Accepted
X-Ivox-Chars-Charged: 39
{
"id": "9f2b41c8-3d5e-4a17-8c60-2e7b1d904f33",
"object": "job",
"type": "tts",
"status": "queued",
"livemode": true,
"created_at": "2026-09-09T11:20:31Z",
"updated_at": "2026-09-09T11:20:31Z",
"usage": { "chars_estimated": 39, "chars_charged": 39, "chars_refunded": 0 },
"result": null,
"error": null
}
# GET /v1/tts/9f2b41c8-... по готовности:
{
"status": "succeeded",
"result": {
"audio_url": "https://ivoxstudio.ru/api/v1/files/v2.k7Qm2xR9vT4pL8wN3sZ6bC1dF5hJ….mp3",
"audio_expires_at": "2026-09-10T11:20:31+00:00",
"duration_sec": 4,
"format": "mp3",
"model": "ivox-expressive",
"library_id": "6c0f2a91-4d18-4f0a-9c22-7b5e1a3d8e40",
"share_id": "aB7kQ2"
}
}Оговорки
Теги эмоций в квадратных скобках — [задумчиво], [смеётся] — понимает только ivox-expressive. На остальных моделях они будут прочитаны вслух как текст.
Синхронного потокового синтеза в API нет: POST /v1/tts/stream не существует. Любой вызов создаёт задачу, ответ 202, аудио забирается по ссылке из результата.
Словарь произношений аккаунта применяется к синтезу ВСЕГДА и выключателя не имеет: поле dictionary в теле не принимается. Управлять правилами через API тоже нельзя — методов /v1/dictionary нет, словарь живёт только в интерфейсе.
Полей metadata и project_id в API нет. Своё сопоставление с заказом держите на Idempotency-Key: он же защищает от двойного списания. Ключ — до 255 символов; тот же ключ с другим телом или на другом инструменте — 409 idempotency_key_reuse. Неизвестное поле в теле — 422 с его именем, а не молчаливое игнорирование. Так задумано: проглоченный параметр заставляет клиента месяцами верить, что мы его учли.
Задачу в очереди можно отменить общим POST /v1/jobs/{id}/cancel — с полным возвратом и статусом canceled. Начавшуюся — нельзя: 409 job_not_cancelable.
Инструментов здесь 14, и это ровно то, что отвечает по ключу. Чего в API нет вовсе — клонирования и дизайна голосов, словаря, проектов, каталога звуков и агентов — перечислено в «Обзоре» отдельным списком, а не обойдено молчанием.
Все методы
Полная поверхность API: 33 методов в 8 группах.
Строка раскрывается — тело запроса, поля с пределами и коды ответов.
Озвучка текста · 6
Основной сценарий. Статус задачи озвучки читается на своём адресе — /tts/{job_id}: на нём уже стоят интеграции.
Голоса · 1
Только чтение: клонирование и дизайн голоса в публичный API не вынесены, они живут в интерфейсе сайта.
Работа со звуком · 5
Файл передаётся загрузкой (multipart, поле file) либо ссылкой (JSON, поле url) — ровно одно из двух. Ссылка — только https:// на публичный адрес, иначе 422 invalid_url ещё до списания. Поминутные инструменты округляют длительность вверх до целой минуты, минимум — минута.
Распознавание · 2
Оба инструмента живут в семействе audio: скоупов speech:read и speech:write не существует.
Видео и аватары · 5
Тарифа нет ни у видео, ни у говорящего аватара, и поэтому оба метода отвечают 403 tool_unavailable. Каталог персонажей (GET/POST/DELETE /avatars) при этом работает и ничего не списывает.
Задачи · 3
Один вход в тринадцать инструментов. Право на задачу выводится из её инструмента: задача sfx читается по audio:read, задача tts — по tts:read.
Вебхуки · 8
Управляются скоупом account:write, читаются account:read. Скоупа webhooks:manage не существует.
Аккаунт · 3
Баланс и расход — те же цифры, что видит личный кабинет.
Какие модели подключены
Выбор есть только у озвучки
У остальных инструментов модель задана нами, и передавать поле model бессмысленно. Звук отдаётся в mp3: в запросе озвучки формат называется mp3_44100_128, в result.format приходит mp3; диалог собирается в wav.
Озвучка текста · 3
выбирается в запросеivox-expressiveПо умолчанию. Единственная понимает теги эмоций в тексте — [задумчиво], [смеётся]. Из настроек принимает только stability: speed, similarity и style на ней ничего не меняют.
ivox-multilingualСтабильная и предсказуемая, 29 языков, без тегов эмоций. Она же аварийная замена, если основная недоступна.
ivox-fastСамая быстрая, без тегов эмоций. Для интерактива, где важна задержка.
Многоголосый диалог
задана намиivox-dialogueЗадана жёстко: склейка реплик в одну дорожку требует механики, которой у выразительной модели нет. Параметр model здесь не принимается, теги эмоций не работают.
Расшифровка
задана намиivox-transcribeРаспознавание речи с разделением по говорящим и таймкодами.
Смена голоса
задана намиivox-voice-changeРечь в речь: сохраняет интонацию, паузы и темп оригинала, меняя только тембр.
Звуковые эффекты
задана намиivox-sfxОписания переводятся на английский перед генерацией: кириллицу движок принимает за речь и озвучивает её вместо создания звука.
Музыка
задана намиivox-musicИнструментал и вокал по описанию, от 3 до 300 секунд.
Видео
задана намиivox-videoСчитается отдельной очередью у стороннего провайдера и долго: ожидание до 7 минут. Потолок длительности — 12 секунд.
Аватары
задана намиivox-avatarЛипсинк по картинке и звуку. Медленнее обычного видео — ожидание до 10 минут.
Имена наши, а не движка. За каждым стоит конкретная модель, и мы оставляем за собой право заменить её на более сильную, не меняя имени. Такая замена меняет звук, и объявлять её мы обязаны заранее — но закрепить за ключом старое звучание вам сейчас нечем: дата-версий и заголовка Ivox-Version в API ещё нет. Пока их не будет, единственная защита от неожиданной смены звука — ваш собственный слепок готовых файлов. Закладываться в коде на конкретный движок не нужно и не получится: в ответе тоже приходит наше имя.
Пакеты
Загружаем тарифы…
Тарификация
Единица — символ, баланс общий с сайтом
Оценка считается при постановке задачи и уточняется по факту: в ответе приходит и списанное, и остаток.
Списываем при постановке задачи по оценке и уточняем по факту. Неуспешная задача возвращает всё списанное, возврат виден в usage.chars_refunded. Фактическое списание и остаток приходят заголовками X-Ivox-Chars-Charged и X-Ivox-Chars-Balance в ответе на постановку. Музыка не исключение: полная цена трека списывается при постановке и возвращается целиком, если задача упала или отменена. Файл по ссылке списывается минутой, а разница — до начала обработки.
Узнать цену заранее, ничего не запуская, можно только для озвучки: POST /estimate принимает type: «tts» и на любое другое значение отвечает 422 с кодом unsupported_type. Для остальных инструментов считайте по ставке из таблицы — она та же, по которой списывает код, и у поминутных округляйте вверх до целой минуты.
Лимиты
Одни для всех ключей
Числа заданы в коде: индивидуального лимита у ключа нет, и поднять его по запросу в поддержку сейчас нечем — планируйте нагрузку по значениям ниже.
Лимита на число одновременно активных задач нет: ставьте столько, сколько пропускает лимитер запросов. 429 приходит только от него — с rate_limit_error и Retry-After; кода too_many_active_jobs в API не существует. Заголовки X-RateLimit-* приходят на каждый запрос с ключом — и на успешный ответ, и на ошибку.
Вебхуки
Подпись HMAC-SHA256, окно защиты от повтора 5 минут, 8 повторов на протяжении ~33 часов, до 5 адресов на каждый режим — боевой и тестовый.
Основной способ узнать, что задача готова.
Мы стучимся на ваш адрес, вы отвечаете 2xx за 10 секунд.
Событие job.succeeded
{
"id": "evt_3Fd8kLq2Wm7Rx9Tb4Nc1Vz",
"type": "job.succeeded",
"created_at": "2026-09-09T11:22:04Z",
"livemode": true,
"data": {
"object": {
"id": "9f2b41c8-3d5e-4a17-8c60-2e7b1d904f33",
"object": "job",
"type": "tts",
"status": "succeeded",
"livemode": true,
"created_at": "2026-09-09T11:20:31Z",
"updated_at": "2026-09-09T11:22:04Z",
"started_at": "2026-09-09T11:20:33Z",
"finished_at": "2026-09-09T11:22:04Z",
"request": { "text": "…", "voice_id": "1", "model": "ivox-expressive" },
"usage": { "chars_estimated": 128, "chars_charged": 128, "chars_refunded": 0 },
"result": {
"audio_url": "https://ivoxstudio.ru/api/v1/files/v2.k7Qm2xR9vT4pL8wN3sZ6bC1dF5hJ….mp3",
"audio_expires_at": "2026-09-10T11:22:04+00:00",
"duration_sec": 9,
"format": "mp3",
"model": "ivox-expressive",
"library_id": "6c0f2a91-4d18-4f0a-9c22-7b5e1a3d8e40",
"share_id": null
},
"error": null
}
}
}События
job.succeededЗадача готова, в result лежит ссылка на файл.job.failedЗадача не получилась, списанное возвращено.job.canceledЗадачу отменили, пока она стояла в очереди.Других событий нет: подписка на неизвестное отклоняется.
Проверка подписи
# Так выглядит наш запрос к вам: POST https://example.com/hooks/ivox Ivox-Signature: t=1789012924,v1=3f1a9c… Ivox-Event-Id: evt_3Fd8kLq2Wm7Rx9Tb4Nc1Vz Ivox-Event-Type: job.succeeded Ivox-Delivery-Attempt: 1 Content-Type: application/json {"id":"evt_3Fd8kLq2Wm7Rx9Tb4Nc1Vz","type":"job.succeeded",...} # Проверка подписи руками. BODY — тело ровно как пришло, без добавленного перевода # строки: printf, а не echo. T=1789012924 BODY='{"id":"evt_3Fd8kLq2Wm7Rx9Tb4Nc1Vz","type":"job.succeeded"}' printf '%s.%s' "$T" "$BODY" \ | openssl dgst -sha256 -hmac "$IVOX_WEBHOOK_SECRET" -r \ | cut -d' ' -f1 # Результат должен совпасть с v1 из заголовка. Не совпал — сверьте, что тело взято # сырым (не пересобранный JSON) и что метка t взята из того же заголовка. # Ответить 2xx за 10 секунд. Иначе повторы: 10 с → 30 с → 2 мин → 10 мин → 30 мин → # 2 ч → 6 ч → 24 ч: восемь повторов, всего девять попыток.
Подпись считается по сырому телу запроса: разобранный и собранный обратно JSON даст другую строку, и она не сойдётся. Во время ротации секрета в заголовке две подписи v1= — проверяйте каждую и принимайте событие, если сошлась любая.
Доставка
Повторы
Что будет, если ваш адрес молчит
Расписание повторов: 10 с → 30 с → 2 мин → 10 мин → 30 мин → 2 ч → 6 ч → 24 ч.
Когда повторы кончились, доставка закрывается провалом, а у адреса растёт счётчик подряд исчерпанных доставок: первая ставит метку «сбоит» (status: failing), двадцатая отключает адрес совсем (disabled). Успешная доставка обнуляет счётчик и снимает failing — но только его. Из disabled адрес сам не выходит. Включает обратно только владелец: PATCH /webhooks/{id} с status: «enabled», он же обнуляет счётчик. Писем об этом мы не шлём: узнать состояние можно из GET /webhooks и из истории доставок.
После rotate_secret прежний секрет живёт ещё 24 часа, и всё это время в Ivox-Signature приходят две подписи: t=…,v1=…,v1=…. Проверяйте каждую и принимайте событие, если сошлась любая, — пример проверки так и делает. Разбор заголовка в словарь оставит одну из двух, и после переезда на новый секрет сутки будут отвергаться все события. Подписка, созданная тестовым ключом, получает события тестовых задач, подписанные её собственным секретом.
Доставка at-least-once: одно и то же событие может прийти дважды — например, когда ваш ответ 2xx не доехал до нас и сработал повтор. Порядок между событиями разных задач тоже не гарантирован. Обработчик обязан быть идемпотентным по Ivox-Event-Id.
Ошибки и версии
Один конверт на все коды. Логику пишите на поле type — текст сообщения может меняться.
В каждом ответе, включая успешные, есть Ivox-Request-Id: в поддержку приходите с ним.
Ответ с ошибкой
{
"error": {
"type": "invalid_request_error",
"code": "invalid_value",
"message": "Значение поля «text» не подходит: String should have at most 5000 characters",
"param": "text",
"request_id": "req_5k7j1PLV6bRFQ1Me3iuhtj",
"doc_url": "https://ivoxstudio.ru/developers#errors-422"
}
}Что стоит знать заранее
Чужой объект отдаётся как 404 нарочно: иначе по ответам можно было бы перебором выяснять, какие идентификаторы существуют.
Коды ответа
15 на весь API
Повторять запрос имеет смысл только там, где это написано: остальные коды при повторе вернут то же самое.
invalid_request_errorТело не разобралось как JSONповтор: нетauthentication_errorКлюча нет, он не наш, отозван или истёкповтор: нетinsufficient_creditsНе хватает символов либо расход заморожен подпиской. Тот же код приходит в error упавшей задачи, если на досчёт длинного файла по ссылке не хватило баланса, — списанное тогда возвращено целикомповтор: после пополненияpermission_errorНет нужного скоупа (insufficient_scope) либо у инструмента не задан тариф (tool_unavailable)повтор: нетnot_found_errorОбъекта нет или он чужойповтор: нетinvalid_request_errorНе тот метод на этом адресеповтор: нетinvalid_request_errorКлюч идемпотентности уже использован с другим телом или на другом инструменте (idempotency_key_reuse), задачу уже нельзя отменить (job_not_cancelable), доставка вебхука уже отправляется (delivery_in_progress) или уже доставлена (delivery_already_succeeded)повтор: нетinvalid_request_errorТело или файл больше пределаповтор: нетinvalid_request_errorunsupported_media_type: JSON-метод получил не application/json, файловый — ни JSON, ни multipart; у персонажа — файл не из jpg, png, webp, mp4, mov, m4v, webmповтор: нетinvalid_request_errorЗапрос не прошёл проверку; поле param называет виновника. Коды: invalid_value, missing_field, invalid_url, invalid_cursor, invalid_range, range_too_wide, text_too_long, batch_too_large, unsupported_voice и другиеповтор: нетrate_limit_errorПревышен лимит запросов на ключповтор: по retry-afterapi_errorНаша поломка; текст маскируется намеренноповтор: с нарастающей паузойprovider_errorДвижок отказал в обработкеповтор: с нарастающей паузойprovider_errorДвижок недоступен или отключён предохранителемповтор: с нарастающей паузойprovider_errorДвижок не ответил вовремя (provider_timeout)повтор: с нарастающей паузойВерсии и совместимость
Мажорная версия — в адресе, /v1
Ради этого публичный API и отделён от внутреннего: тот меняется вместе с сайтом, этот — по правилам.
Что мы можем поменять молча
- Новое поле в ответе
- Новое необязательное поле запроса
- Новое значение в перечислении
- Новый эндпоинт
Только новый мажор
- Удаление поля или эндпоинта
- Новое обязательное поле запроса
- Сужение типа поля
- Смена класса ошибки
Это условие совместимости, а не пожелание. Клиент обязан игнорировать незнакомые поля ответа и незнакомые значения status. Код, который падает на новом поле, сломается на ближайшем совместимом обновлении — и это будет не наша поломка.