Документация API

Yotta STT распознает казахскую, русскую и смешанную речь. Вы отправляете аудио и получаете текст с разметкой по фразам и словам. Базовый URL: https://api.yotta.kz. Все ответы в JSON, ошибки в виде {"detail": "текст"}. Полная схема методов: Swagger.

Быстрый старт

  1. Получите API-ключ у нас (ключи выдаем вручную). Ключ выглядит как yk_live_….
  2. Загрузите файл одной командой. В ответ придет id задачи и ссылка на поток событий.
  3. Получите результат: через поток событий (SSE), запросом GET или webhook.
bash
# 1. загрузить файл
curl -H "X-API-Key: $KEY" -F file=@call.wav https://api.yotta.kz/v1/transcriptions

# 2. следить за распознаванием в реальном времени
curl -N "https://api.yotta.kz/v1/transcriptions/$ID/events?token=$STREAM_TOKEN"

# 3. или забрать готовый результат
curl -H "X-API-Key: $KEY" https://api.yotta.kz/v1/transcriptions/$ID

Время обработки: около 0.3 с на 1 с аудио. Фраза 10 с распознается примерно за 5 с, запись 40 с за 13-15 с. Если очередь занята, дольше.

Авторизация

СпособКак передатьДля чего
API-ключX-API-Key: yk_live_…Ваш сервер. Все методы.
Временный upload-токенX-Upload-Token: ut.…Браузер. Только загрузка файлов.
Токен потока?token=<stream_token>Браузер. Только SSE одной задачи или пачки.
Не передавайте API-ключ в браузер или мобильное приложение. Если файлы загружаются прямо из браузера, получите на своем сервере временный upload-токен и отдайте его клиенту.

Временный upload-токен

Выпускается вашим сервером по API-ключу. Токен многоразовый в пределах срока жизни, файлы списываются с аккаунта ключа. ttl_s: от 60 до 3600 секунд. max_duration_s: максимальная длительность одного файла (для более длинных вернется 413).

curl -X POST https://api.yotta.kz/v1/upload-tokens \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"ttl_s": 600, "max_duration_s": 300}'

# 201 {"upload_token": "ut.acc_….1794….300.ab12…", "expires_at": 1794509204, "max_duration_s": 300}

Загрузка

Один файл

POST /v1/transcriptions, multipart/form-data. Заголовок X-API-Key или X-Upload-Token.

ПолеОписание
fileАудиофайл, обязательно
mergetrue | false, по умолчанию true. Слияние двух моделей через LLM: точнее, но чуть дольше.
languageauto | kk | ru. Пока не используется, язык определяется автоматически.
metadataJSON-строка, вернется в результате без изменений (например, id кассы или звонка)
webhook_urlКуда прислать результат после завершения (см. Webhook)
curl -H "X-API-Key: $KEY" \
  -F file=@call.wav \
  -F merge=true \
  -F 'metadata={"store":"almaty-12"}' \
  -F webhook_url=https://example.kz/yotta/webhook \
  https://api.yotta.kz/v1/transcriptions

Ответ 202:

json
{
  "id": "tr_1a1d…",
  "status": "queued",
  "filename": "call.wav",
  "duration_s": 10.8,
  "billed_s": 11,
  "stream_url": "https://api.yotta.kz/v1/transcriptions/tr_1a1d…/events",
  "stream_token": "1794509204.39dc0e57…"
}

Прогресс загрузки большого файла в браузере доступен через XMLHttpRequest.upload.onprogress (у fetch его нет).

Пачка файлов

POST /v1/batches, поле files повторяется. Остальные поля такие же. Файлы, которые не удалось принять, отмечаются в ответе как rejected, остальные обрабатываются.

curl -H "X-API-Key: $KEY" \
  -F files=@a.wav -F files=@b.mp3 -F files=@c.ogg \
  https://api.yotta.kz/v1/batches

# 202 {"batch_id": "bt_…",
#      "items": [ {…как для одного файла…}, {"filename": "x.txt", "status": "rejected", "error": "…"} ],
#      "stream_url": "…/v1/batches/bt_…/events", "stream_token": "…"}

Форматы и лимиты

  • Форматы: wav, mp3, ogg, opus, m4a, flac, webm, aac, mp4.
  • Размер файла: до 200 МБ.
  • Длительность: ограничивается upload-токеном (max_duration_s).
  • Если у аккаунта слишком много задач в очереди, вернется 429: повторите запрос позже.

Получение результата

SSE: события в реальном времени

Откройте stream_url с ?token=<stream_token> (или с заголовком X-API-Key на сервере). Токен потока живет 1 час.

СобытиеdataЧто делать
status{id, status:"queued", position}Задача в очереди
status{id, status:"processing", attempt}Распознавание началось
status{id, status:"queued", retry:true, reason}Повтор после сбоя, фразы придут заново
progress{id, stage:"asr", percent}Прогресс
segment{id, segment:{…}}Upsert фразы по segment.id, сортировка по start
doneполный результатПоказать результат, закрыть поток
error{id, status:"failed", code, message}Показать ошибку, закрыть поток
  • Каждая фраза приходит минимум дважды: черновик (merged: false) и итог (merged: true). После повтора задачи еще раз. Заменяйте фразу по segment.id, не добавляйте новую.
  • Порядок фраз не гарантирован, сортируйте по start.
  • При обрыве EventSource переподключается сам, сервер дошлет пропущенное по Last-Event-ID. При новом подключении придет вся история задачи.
  • После done или error сервер закрывает поток. Закрывайте EventSource в этих обработчиках, иначе браузер попробует переподключиться.
  • Раз в 15 с приходит комментарий : ping для поддержания соединения.
const segments = new Map();
const es = new EventSource(`${task.stream_url}?token=${task.stream_token}`);

es.addEventListener("status", (e) => {
  const d = JSON.parse(e.data); // queued(position) | processing | queued + retry
});
es.addEventListener("progress", (e) => {
  const { percent } = JSON.parse(e.data);
});
es.addEventListener("segment", (e) => {
  const { segment } = JSON.parse(e.data);
  segments.set(segment.id, segment);                       // upsert
  render([...segments.values()].sort((a, b) => a.start - b.start));
});
es.addEventListener("done", (e) => {
  const result = JSON.parse(e.data);
  es.close();
});
es.addEventListener("error", (e) => {
  if (e.data) {                     // ошибка задачи, а не обрыв сети
    const err = JSON.parse(e.data);
    es.close();
  }
});

Поток пачки (/v1/batches/{batch_id}/events) присылает события всех задач пачки, поле id в событии это id задачи. Поток закрывается, когда завершены все задачи.

GET: опрос статуса

МетодПутьОтвет
GET/v1/transcriptions/{id}статус или полный результат; для queued есть position
GET/v1/transcriptions?status=&limit=список задач аккаунта (без фраз)
GET/v1/transcriptions/{id}/eventsSSE
GET/v1/transcriptions/{id}/export?format=файл txt | srt | vtt | json
DELETE/v1/transcriptions/{id}204, задача и результат удалены
GET/v1/batches/{batch_id}{counts: {done: 3, …}, items: […]}
GET/v1/balance{balance_s, balance_h}
curl -H "X-API-Key: $KEY" https://api.yotta.kz/v1/transcriptions/$ID

Webhook

Если при загрузке указан webhook_url, после завершения задачи мы отправим туда POST. При неудаче до 5 попыток с паузами 2-32 с. Отвечайте 2xx как можно быстрее.

http
POST <webhook_url>
X-Yotta-Timestamp: 1794509204
X-Yotta-Signature: sha256=<HMAC-SHA256(webhook_secret, timestamp + "." + body)>
Content-Type: application/json

{"event": "transcription.done" | "transcription.failed", "data": { …результат как в GET… }}

webhook_secret выдается при создании аккаунта. Проверяйте подпись по сырому телу запроса и отклоняйте запросы со старым X-Yotta-Timestamp (например, старше 5 минут).

import crypto from "node:crypto";

// Next.js route handler: app/api/yotta/webhook/route.ts
export async function POST(req: Request) {
  const raw = await req.text();
  const ts = req.headers.get("x-yotta-timestamp") ?? "";
  const sig = req.headers.get("x-yotta-signature") ?? "";

  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.YOTTA_WEBHOOK_SECRET!)
    .update(`${ts}.${raw}`)
    .digest("hex");

  const ok = sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  if (!ok || !fresh) return new Response("bad signature", { status: 401 });

  const { event, data } = JSON.parse(raw);
  // event: "transcription.done" | "transcription.failed"
  return new Response("ok");
}

Формат результата

json
{
  "id": "tr_1a1d0e9d5ee6fe6fbd2",
  "status": "done",
  "batch_id": null,
  "filename": "t3_001.wav",
  "created_at": "2026-11-12T17:59:08.498143+00:00",
  "finished_at": "2026-11-12T17:59:58.101+00:00",
  "duration_s": 39.99,
  "billed_s": 40,
  "params": {"language": "auto", "diarization": false, "merge": true},
  "metadata": {"store": "almaty-12"},
  "text": "нормально, еле-еле мне нужны которые …",
  "speech_s": 31.85,
  "languages": ["kk", "ru"],
  "speakers": [],
  "segments": [
    {
      "id": 1, "start": 1.91, "end": 3.27,
      "text": "мне нужны которые",
      "language": "ru", "language_confidence": 1.0,
      "confidence": 0.812,
      "speaker": null, "speaker_confidence": null,
      "merged": true,
      "words": [
        {"w": "мне", "start": 1.99, "end": 2.11, "conf": 0.888},
        {"w": "которые", "start": 2.63, "end": 2.95, "conf": 0.7, "edited": true}
      ]
    }
  ],
  "model": {"asr": "gigaam-ml+k5", "merge": "…", "worker_version": "0.1.0"},
  "timings": {"vad_s": 0.3, "asr_s": 11.6, "total_s": 12.8, "rtf": 0.32}
}
ПолеЗначение
statusqueued → processing → done / failed
textВесь текст одной строкой
duration_s / speech_sДлина файла / сколько в нем речи, секунды
billed_sСколько секунд кредитов списано
languagesЯзыки в записи: kk, ru
segments[]Фразы между паузами. id: номер фразы, start/end: секунды от начала файла
segment.languagekk / ru / mixed
segment.mergedfalse: черновик, true: итог после слияния
words[].confУверенность слова, от 0 до 1. Ниже 0.5 стоит проверить, 0.5-0.8 скорее верно
words[].editedСлово исправлено ИИ (ни одна модель не дала его в таком виде)
speakers, speaker*Разделение говорящих: пока пусто и null
metadataТо, что вы передали при загрузке
errorДля failed: {code, message}

Экспорт

GET /v1/transcriptions/{id}/export?format=txt|srt|vtt|json возвращает готовый файл. Для незавершенной задачи вернется 409.

bash
curl -H "X-API-Key: $KEY" -o call.srt "https://api.yotta.kz/v1/transcriptions/$ID/export?format=srt"

Формат SRT: номера с 1, время HH:MM:SS,mmm, между блоками пустая строка.

srt
1
00:00:01,910 --> 00:00:03,270
мне нужны которые

VTT: то же с заголовком WEBVTT, точкой вместо запятой в миллисекундах и без номеров.

Ошибки

Тело ошибки: {"detail": "текст на русском"}. Текст можно показывать пользователю.

КодКогда
401Нет или неверен ключ / токен, токен просрочен
402Не хватает кредитов
404Задача не найдена
409Экспорт еще не готовой задачи
413Файл больше 200 МБ или длиннее лимита
415Формат не поддерживается
422Не аудио или неверные параметры
429Слишком много задач в очереди у аккаунта, повторите позже

Кредиты

  • 1 кредит = 1 секунда присланного аудио (округляется вверх, см. billed_s).
  • Кредиты блокируются при загрузке и списываются после успешного распознавания.
  • Если задача завершилась ошибкой, кредиты возвращаются.
  • Баланс: GET /v1/balance → {balance_s, balance_h}. Пополнение по счету, напишите нам.
bash
curl -H "X-API-Key: $KEY" https://api.yotta.kz/v1/balance