Evom Labs API Reference

Developer platform

Loli 2.0 Speech-to-Text API

Public API contract for file transcription, NDJSON progress and real-time speech recognition over a WebSocket.

On this page

Endpoints

POST/api/v1/stt/transcriptions

Transcribes one audio file and returns the result as JSON.

POST/api/v1/stt/transcriptions?stream=true

Transcribes an audio file and returns progress and results as NDJSON lines.

WS/api/public/v1/stt/ws?token=<stt_key>

Takes binary PCM16 and returns partial, segment and final results in real time.

GET/api/public/v1/auth/check

Checks the status of an STT API key.

Needs verification: public gateway configuration

The two paths /api/public/v1/auth/check and /api/public/v1/stt/ws belong to the STT gateway. Whether they are exposed on the public origin depends on the edge configuration of each deployment. If the gateway is not exposed directly, use the origin your operator provides instead of the examples below.

File transcription

POST/api/v1/stt/transcriptions

Send multipart/form-data and receive the full transcription when it finishes.

Form fields

FieldTypeRequiredDefaultDescription
audiofileYes-Audio file, up to 100 MiB. file is accepted as an alias.
languagestringNoautoauto, one of the 30 single-language modes, or one of the three bilingual modes listed below.
streambooleanNofalseQuery parameter. Set true to receive NDJSON progressively.

Containers recognised at present: WAV 16-bit PCM, MP3, WebM, OGG and MP4 or M4A. Decoded duration must be between about 0.2 and 1,800 seconds. Which encoded formats decode depends on the deployment, so do not rely on the filename extension alone.

Examples

curl
curl -X POST "https://studio.evomlabs.com/api/v1/stt/transcriptions" \
  -H "Authorization: Bearer stt_sk_live_YOUR_KEY" \
  -F "audio=@recording.wav" \
  -F "language=auto"
Python
import requests

with open("recording.wav", "rb") as audio:
    response = requests.post(
        "https://studio.evomlabs.com/api/v1/stt/transcriptions",
        headers={"Authorization": "Bearer stt_sk_live_YOUR_KEY"},
        files={"audio": audio},
        data={"language": "auto"},
        timeout=1800,
    )

response.raise_for_status()
print(response.json()["text"])

Response

JSON
{
  "text": "Nội dung đã phiên âm",
  "language": "vi",
  "duration_ms": 1234,
  "provider": "loli-asr",
  "model": "Loli 2.0"
}

Important

A successful response does not use the ok/data envelope; it returns the transcription object directly. duration_ms is processing latency, not the length of the audio.

Language modes

ModeLanguages accepted
autoEvery supported language; the model detects it for you
vi🇻🇳 Vietnamese
en🇬🇧 English
zh🇨🇳 Chinese
yue🇭🇰 Cantonese
ja🇯🇵 Japanese
ko🇰🇷 Korean
th🇹🇭 Thai
id🇮🇩 Indonesian
ms🇲🇾 Malay
tl🇵🇭 Filipino
hi🇮🇳 Hindi
ar🇸🇦 Arabic
fa🇮🇷 Persian
ru🇷🇺 Russian
mk🇲🇰 Macedonian
el🇬🇷 Greek
tr🇹🇷 Turkish
de🇩🇪 German
fr🇫🇷 French
es🇪🇸 Spanish
pt🇵🇹 Portuguese
it🇮🇹 Italian
nl🇳🇱 Dutch
pl🇵🇱 Polish
cs🇨🇿 Czech
ro🇷🇴 Romanian
hu🇭🇺 Hungarian
sv🇸🇪 Swedish
da🇩🇰 Danish
fi🇫🇮 Finnish
en-vi🇬🇧 / 🇻🇳 English and Vietnamese
ko-en🇰🇷 / 🇬🇧 Korean and English
ja-en🇯🇵 / 🇬🇧 Japanese and English

Note

The language field in a response is a label inferred from the transcript or from a single-language mode. It can differ from the mode you sent, and an empty transcript in a multilingual mode can return unknown.

NDJSON progress

POST/api/v1/stt/transcriptions?stream=true

Emits one complete JSON object per line as each chunk finishes, instead of waiting for the whole file. The response carries Content-Type application/x-ndjson.

curl
curl -N -X POST "https://studio.evomlabs.com/api/v1/stt/transcriptions?stream=true" \
  -H "Authorization: Bearer stt_sk_live_YOUR_KEY" \
  -F "audio=@meeting.mp3" \
  -F "language=vi"

Response

NDJSON
{"type":"started","duration_seconds":42.5,"chunks_total":2,"provider":"loli-asr","model":"Loli 2.0"}
{"type":"chunk","index":1,"chunks_total":2,"text":"...","combined_text":"...","language":"vi"}
{"type":"chunk","index":2,"chunks_total":2,"text":"...","combined_text":"...","language":"vi"}
{"type":"final","text":"...","language":"vi","is_final":true,"provider":"loli-asr","model":"Loli 2.0","latency_ms":5230}

Long audio is split into chunks of at most about 25 seconds, cut at a silent point near the boundary where possible. Read the type and chunks_total fields rather than deriving the number of chunks from the duration.

NDJSON
{
  "type": "error",
  "error": {
    "code": "stt_upstream_error",
    "message": "Error description"
  }
}

A stream can end with an error

If the pipeline fails after the response has started, the last line is an error object instead of a final one. Read line by line and handle both final and error.

Real-time WebSocket

WS/api/public/v1/stt/ws?token=<stt_key>&language=auto

Choose automatic VAD or manual commits for a continuous PCM16 microphone stream.

URL
wss://studio.evomlabs.com/api/public/v1/stt/ws?token=stt_sk_live_YOUR_KEY&language=auto

Start message

JSON
{
  "type": "start",
  "mode": "auto",
  "language": "auto",
  "sample_rate": 48000,
  "format": "pcm_s16le"
}
FieldTypeRequiredDefaultDescription
typestringYes-Use start.
mode"auto" | "manual"Noautoauto uses server VAD; manual waits for a client commit.
languagestringNoautoOne of the language modes listed above.
sample_rateintegerNo16000Integer binary-input rate from 8,000 to 192,000 Hz.
formatstringNopcm_s16leExactly pcm_s16le: mono signed 16-bit little-endian PCM.
session_idstringNoServer generatedOptional client-provided identifier.

In auto mode, send binary PCM16 mono continuously. The server resamples to the rate reported in started, detects speech and silence, sends replaceable partial events, and commits segment events at a silence boundary.

Send stop before closing

Send a stop message to end the session. The server then emits final, followed by closed. Closing the socket without sending stop cancels the session and does not guarantee a final transcript.

Manual mode for Push-to-Talk or client VAD

Start with mode manual. The server does not emit partials or detect silence. It buffers PCM after the previous commit and returns exactly one final segment when the client sends commit. A commit needs at least 0.2 seconds of audio; an empty or short commit produces a recoverable bad_request error, and a short buffer remains for more PCM. stop commits a sufficiently long trailing buffer, discards a shorter fragment, then sends final and closed.

JavaScript
// Start with: { "type": "start", "mode": "manual", ... }
// Send PCM16 frames, then commit exactly that buffered speech:
ws.send(JSON.stringify({ type: "commit" }));

// The server returns one final segment. Send commit again for the next turn,
// or stop to commit a sufficiently long trailing buffer and close the session.
ws.send(JSON.stringify({ type: "stop" }));

Server events

JSON
// Server to client

{ "type": "started", "session_id": "session_id", "mode": "auto", "language": "auto",
  "sample_rate": 16000, "provider": "loli-asr", "model": "Loli 2.0" }

{ "type": "partial", "index": 0, "text": "live window text",
  "committed_text": "", "live_text": "live utterance text",
  "combined_text": "live utterance text", "is_final": false,
  "language": "vi", "latency_ms": 210 }

{ "type": "segment", "index": 0, "text": "committed utterance",
  "committed_text": "committed utterance", "live_text": "",
  "combined_text": "committed utterance", "is_final": true,
  "language": "vi", "latency_ms": 350 }

{ "type": "final", "text": "Toàn bộ transcript", "language": "vi",
  "is_final": true, "provider": "loli-asr", "model": "Loli 2.0",
  "latency_ms": 4100 }

{ "type": "closed", "session_id": "session_id", "reason": "stopped" }

{ "type": "error", "session_id": "session_id-or-null",
  "message": "Error description", "code": "bad_request" }

Note

A partial can be replaced by a newer partial for the same utterance; a segment commits one utterance at a silence boundary. Use combined_text to display the transcript so far.

Examples

JavaScript
const ws = new WebSocket(
  "wss://studio.evomlabs.com/api/public/v1/stt/ws" +
  "?token=stt_sk_live_YOUR_KEY&language=auto"
);

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({
    type: "start",
    mode: "auto",
    language: "auto",
    sample_rate: 48000,
    format: "pcm_s16le",
  }));
});

ws.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);
  if (message.type === "partial" || message.type === "segment") {
    renderTranscript(message.combined_text);
  }
  if (message.type === "final") {
    renderTranscript(message.text);
  }
  if (message.type === "error") {
    console.error(message.code, message.message);
  }
});

// Send PCM16 frames with ws.send(arrayBuffer).
function stopTranscription() {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "stop" }));
  }
}

Security

The key travels in the query string, so it is exposed if the connection is opened from client-side code. Open the socket from your own server, over TLS. A missing or wrong key emits the error code unauthorized and the socket closes with code 1008.

A real-time session uses the same audio ceiling of 1,800 seconds.

Evom Labs API documentation