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/voices/{id}Deletes an account-owned custom voice and its bound voice key.
/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.
/api/v1/keysLists account-owned Admin, TTS and STT API-key metadata with remaining usage.
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.
Delete a custom voice
/api/v1/voices/{id}Deletes one custom voice. The account key must allow audio-library management and the voice must be inside its configured voice scope.
Preset voices, another account's voices, missing identifiers and voices outside the key's allow-list all return 403 VOICE_NOT_ALLOWED. A successful deletion also removes the bound voice key; generation history remains.
curl -X DELETE https://studio.evomlabs.com/api/v1/voices/VOICE_ID -H "Authorization: Bearer vc_ak_live_YOUR_KEY"Response
{
"ok": true,
"data": {
"voice_id": "VOICE_ID",
"name": "Authorized sample voice",
"deleted": true
}
}Needs verification
API keys and usage
/api/v1/keysReturns metadata and usage for every Admin, TTS and STT key owned by the account. The caller must be an active account key with keyManagement enabled.
Security
curl https://studio.evomlabs.com/api/v1/keys -H "Authorization: Bearer vc_ak_live_YOUR_KEY"Response
{
"ok": true,
"data": {
"account_usage": {
"tts": {"limit":500000,"used":1234,"remaining":498766,"unlimited":false},
"stt": {"limit":100000,"used":50,"remaining":99950,"unlimited":false},
"reset_date": "2026-09-01T00:00:00.000Z",
"reset_interval": "month",
"reset_interval_count": 1
},
"keys": {"admin":[],"tts":[],"stt":[]}
}
}| Field | Type | Required | Description |
|---|---|---|---|
account_usage | object | Yes | Plan allowance, split into tts and stt. Each includes limit, used, remaining and unlimited. Reset metadata is returned alongside them. |
keys | object | Yes | Always contains admin, tts and stt arrays. An array can be empty. |
Per-key usage fields
| Field | Type | Required | Description |
|---|---|---|---|
credit_limit | integer | null | Yes | Positive lifetime ceiling for this key, or null when this key has no separate ceiling. |
credit_used | integer | Yes | Characters used by this key over its lifetime. |
credit_remaining | integer | null | Yes | max(credit_limit - credit_used, 0), or null when credit_limit is null. |
Admin key permission values
permissions.endpoints.tts, permissions.endpoints.stt, permissions.audioUpload and permissions.keyManagement are booleans. permissions.voiceScope is exactly all, mine or custom. permissions.voiceIds is an array of at most 200 IDs and must not be empty when voiceScope is custom.
allminecustomBatch generation
/api/v1/tts/generateWaits until the audio is ready, then returns a URL to download it. A voice key already selects its voice; an account key must provide voice_id and pass its TTS and voice-scope permissions.
Request
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
text | string | Yes | - | Text to speak, not empty, up to 5,000 characters. |
voice_id | string | No | - | Required with an account key. Omit with a voice key; if provided, it must match the key-bound voice. |
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. |
preprocess_prompt | boolean | true | Reference prompt preprocessing. |
postprocess_output | boolean | true | Generated audio postprocessing. |
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
}
}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.
| Field | Type | Required | Description |
|---|---|---|---|
text_length | integer | Yes | Integer from 1 through 10,000; allowance is deducted when the token is issued. |
voice_id | string | No | Required with an account key. Omit with a voice key; if provided, it must match the key-bound voice. |
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")