Developer platform
Loly 3.5 Text-to-Speech API
Public API contract for voice enrollment, durable batch output, binary bytes, Server-Sent Events, WebSocket streaming and usage.
On this page
Endpoints
/api/v1/voicesCreates a custom voice, and issues a new voice key where appropriate.
/api/v1/tts/generateGenerates one finished audio file from text.
/api/v1/tts/bytesGenerates one finished audio file and returns its binary bytes.
/api/v1/tts/sseStreams PCM16 audio events over a POST response.
/api/v1/tts/stream-tokenIssues a short-lived token that opens the streaming WebSocket.
/ws/tts/stream?token=<stream_token>Delivers PCM16 audio as binary frames.
/api/v1/usageReads the allowance of the account behind the voice key.
Needs verification: public exposure
Voice enrollment
/api/v1/voicesCreates a voice from an audio sample and returns its voice key. The account key authorises the call; the new voice key is returned once, in plaintext.
Form fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
file | file | Yes | - | Audio file, up to 10 MiB. |
name | string | Yes | - | Voice name, not empty, up to 64 characters. |
consent | string | Yes | - | Must be true. Confirms the speaker authorised the clone. |
description | string | No | - | Free-text note stored with the voice. |
gender | string | No | female | One of male, female or other. |
Accepted when the MIME type starts with audio/, or when the extension is one of:
m4amp3wavwaveaacoggogaopusflacwebaaiffaifaifccafwmaamrmp4m4bConsent is required
Examples
curl -X POST https://studio.evomlabs.com/api/v1/voices \
-H "Authorization: Bearer vc_ak_live_YOUR_KEY" \
-F "file=@sample.wav" \
-F "name=Authorized sample voice" \
-F "gender=female" \
-F "consent=true"Response
{
"ok": true,
"data": {
"voice_id": "voice_id",
"name": "Authorized sample voice",
"status": "ready",
"deduped": false,
"gender": "female",
"description": "",
"file_name": "sample.wav",
"file_size": 482310,
"created_at": "2026-08-03T00:00:00.000Z",
"key_status": "created",
"api_key": {
"key": "vc_sk_live_NEW_KEY",
"last_four": "ABCD",
"scopes": ["tts.generate", "tts.stream", "usage.read"]
}
}
}Duplicates and retries
A regular account may enroll up to 20 voices through the public route, at up to 6 enrollments per minute. Exceeding the rate returns 429 with a Retry-After header.
Batch generation
/api/v1/tts/generateWaits until the audio is ready, then returns a URL to download it. The voice comes from the key, so the payload carries no voice identifier.
Request
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
text | string | Yes | - | Text to speak, not empty, up to 5,000 characters. |
language | string | No | auto | auto, or one code or full name from the 646-language catalog. |
format | string | No | mp3 | Exactly mp3 or wav. The returned container and MIME match this value. |
speed | number | No | 1.0 | Playback rate, clamped to 0.5 to 1.5. |
cfg_value | number | No | 2.0 | How closely to follow the reference voice. A finite number from 0.0 to 4.0, including 0. |
dit_steps | integer | No | 10 | Diffusion steps. An integer from 0 to 64, including 0. |
The allowance is deducted up front by text.length, including any whitespace in the string you send.
Advanced parameters
These fields are part of the public contract and are accepted by the endpoint. Leave them at their defaults unless you have a reason to change them.
| Field | Type | Default | Description |
|---|---|---|---|
do_normalize | boolean | false | Text normalisation before synthesis. |
denoise | boolean | false | Denoising, on the public batch route. |
control_instruction | string | "" | Not used to select a voice other than the one bound to the key. |
use_prompt_text | boolean | false | Advanced compatibility field. |
prompt_text | string | "" | Advanced compatibility field. |
Examples
curl -X POST https://studio.evomlabs.com/api/v1/tts/generate \
-H "Authorization: Bearer vc_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Xin chà o, đây là bà i test giọng nói.",
"language": "vi",
"format": "wav",
"cfg_value": 2.0,
"dit_steps": 10
}'import requests
BASE_URL = "https://studio.evomlabs.com"
API_KEY = "vc_sk_live_YOUR_KEY"
res = requests.post(
f"{BASE_URL}/api/v1/tts/generate",
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json={
"text": "Xin chà o, đây là bà i test giọng nói.",
"language": "vi",
"format": "wav",
},
)
data = res.json()
if data.get("ok"):
print("Audio URL:", data["data"]["audio_url"])
else:
print("Error:", data["error"]["message"])const BASE_URL = "https://studio.evomlabs.com";
const API_KEY = "vc_sk_live_YOUR_KEY";
const res = await fetch(`${BASE_URL}/api/v1/tts/generate`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Xin chà o, đây là bà i test giọng nói.",
language: "vi",
format: "wav",
}),
});
const { ok, data, error } = await res.json();
if (ok) console.log(data.audio_url);
else console.error(error.message);Response
{
"ok": true,
"data": {
"success": true,
"request_id": "cmp3sih7p0002v9o670rd59xc",
"voice_id": "clx8abc123def",
"audio_url": "https://cdn.example.com/v1_tts/user123/audio.wav",
"text": "Xin chà o, đây là bà i test giọng nói.",
"duration": null,
"status": "completed",
"chars_deducted": 34
}
}Complete Loly 3.5 language catalog — `language` accepts `auto`, one catalog code, or its full name.
Audio bytes
/api/v1/tts/bytesUses the same JSON fields and permissions as batch generation, but returns the completed file directly.
The server finishes synthesis, returns audio bytes without a JSON success envelope, and does not persist a second R2 copy. Errors still use the REST error envelope. Save the response body using the selected format.
curl -X POST https://studio.evomlabs.com/api/v1/tts/bytes \
-H "Authorization: Bearer vc_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Xin chĂ o","language":"vi","format":"wav"}' \
--output hello.wav| Header | Value | Meaning |
|---|---|---|
Content-Type | audio/wav | audio/mpeg | WAV for wav, MPEG audio for mp3. |
Content-Disposition | attachment filename | Suggested oriagent.wav or oriagent.mp3 filename. |
X-OriAgent-Format | wav | mp3 | The selected output format. |
X-OriAgent-Sample-Rate | integer Hz | Present only when the backend provides it. |
X-OriAgent-Chars-Deducted | integer | Exactly text.length charged for the request. |
SSE audio stream
/api/v1/tts/sseUses the same JSON fields as batch generation and returns progressive text/event-stream events.
SSE is text-only, so every audio event contains base64 PCM16 little-endian mono. format is still validated as wav or mp3 for a shared request contract, but it does not change SSE frames. Use fetch plus a ReadableStream because EventSource only supports GET.
curl -N -X POST https://studio.evomlabs.com/api/v1/tts/sse \
-H "Authorization: Bearer vc_sk_live_YOUR_KEY" \
-H "Accept: text/event-stream" \
-H "Content-Type: application/json" \
-d '{"text":"Xin chĂ o","language":"vi"}'Server events
event: start
data: {"sample_rate":24000,"channels":1,"format":"pcm_s16le","encoding":"base64"}
event: audio
data: {"sequence":1,"audio":"<base64 PCM16 little-endian>"}
event: done
data: {"chunks":12,"duration_ms":5230,"elapsed_ms":6100}Important
Stream token
/api/v1/tts/stream-tokenExchanges the voice key for a short-lived token that opens the WebSocket. The allowance is deducted here.
text_length must be greater than 0 and no larger than 10,000. Send the length of the text you actually intend to stream.
curl -X POST https://studio.evomlabs.com/api/v1/tts/stream-token \
-H "Authorization: Bearer vc_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"text_length": 200}'Response
{
"ok": true,
"data": {
"stream_token": "SHORT_LIVED_TOKEN",
"ws_url": "wss://studio.evomlabs.com/ws/tts/stream",
"max_length": 200,
"chars_deducted": 200,
"expires_in": 60
}
}The token lasts 60 seconds
max_length returned with the token, and anything longer is rejected. The allowance is deducted when the token is issued and is not currently refunded if the token goes unused or the stream is cancelled.Always connect to ws_url
WebSocket
/ws/tts/stream?token=<stream_token>Streams PCM16 mono at 24 kHz, in frames of roughly 200 ms.
Start message
{
"type": "start",
"text": "Xin chà o, đây là bà i test giọng nói.",
"language": "vi",
"speed": 1.0,
"control_instruction": "",
"cfg_value": 2.0,
"dit_steps": 8,
"use_prompt_text": false,
"prompt_text": "",
"do_normalize": false,
"denoise": true,
"preprocess_prompt": true,
"postprocess_output": true
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | - | Must be start. |
text | string | Yes | - | Must not exceed the max_length carried by the token. |
language | string | No | auto | auto, or one code or full name from the 646-language catalog. |
speed | number | No | 1.0 | Clamped to 0.5 to 1.5. |
cfg_value | number | No | 2.0 | Range 0.0 to 4.0. |
dit_steps | integer | No | 8 | Range 0 to 64. |
control_instruction | string | No | "" | Ignored while the stream uses a cloned-voice prompt. |
use_prompt_text | boolean | No | false | Advanced compatibility field. |
prompt_text | string | No | "" | Advanced compatibility field. |
do_normalize | boolean | No | false | Text normalisation before synthesis. |
denoise | boolean | No | true | Denoising, on the public batch route. |
preprocess_prompt | boolean | No | true | Reference prompt preprocessing. |
postprocess_output | boolean | No | true | Output postprocessing. |
Important
Server events
// Server to client
{ "type": "start", "mode": "fast", "sample_rate": 24000,
"format": "pcm16", "channels": 1 }
<binary frame> // PCM16 mono, about 200 ms per frame
{ "type": "done", "mode": "fast", "audio_url": "/api/tts/file/output.wav",
"chunks": 12, "ttfb_ms": 480, "duration_ms": 5230 }
{ "type": "error", "message": "Error description" }
{ "type": "cancelled" }Binary frames and JSON frames
A missing, invalid or expired token produces an error event, after which the server closes the socket with code 4401. Close the socket to cancel a generation.
Examples
import json, requests, websocket
BASE_URL = "https://studio.evomlabs.com"
API_KEY = "vc_sk_live_YOUR_KEY"
text = "Xin chà o, đây là bà i test giọng nói."
# 1. Ask for a short-lived token. It expires in 60 seconds, so open the
# socket immediately afterwards.
token = requests.post(
f"{BASE_URL}/api/v1/tts/stream-token",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"text_length": len(text)},
).json()["data"]
# 2. Always connect to the ws_url the response returned.
ws = websocket.create_connection(f"{token['ws_url']}?token={token['stream_token']}")
ws.send(json.dumps({"type": "start", "text": text, "language": "vi"}))
pcm = bytearray()
while True:
frame = ws.recv()
if isinstance(frame, bytes):
pcm.extend(frame) # PCM16 mono at 24 kHz
continue
event = json.loads(frame)
if event["type"] in ("done", "error", "cancelled"):
break
ws.close()
print("received", len(pcm), "bytes of audio")