Theme
Text to Speech
Generate mono, 16-bit PCM WAV audio at 24 kHz. Use a server-side API key with tts:write permission.
Generate speech
Generate a complete WAV file from text.
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | Yes | 1 to 1,000 UTF-8 bytes |
language | string | Yes | Code returned by /v1/tts/languages |
voice_id | string | Yes | Opaque voice_... ID from /v1/voices |
speed | number | No | 0.5 to 2.0, default 1 |
Send a unique Idempotency-Key with each new generation. Reuse that key only when retrying the same body.
bash
curl -fS 'https://api.voicelab.uz/v1/tts' \
-H "Authorization: Bearer $VOICELAB_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: hello-world-001' \
-d '{
"text": "Hello from VoiceLab.",
"language": "en",
"voice_id": "voice_01J9NEUTRAL0000000000000001",
"speed": 1
}' \
-o hello.wavpython
import os
import requests
response = requests.post(
"https://api.voicelab.uz/v1/tts",
headers={
"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}",
"Idempotency-Key": "hello-world-001",
},
json={
"text": "Hello from VoiceLab.",
"language": "en",
"voice_id": "voice_01J9NEUTRAL0000000000000001",
"speed": 1,
},
timeout=60,
)
response.raise_for_status()
open("hello.wav", "wb").write(response.content)ts
import { writeFile } from "node:fs/promises";
const response = await fetch("https://api.voicelab.uz/v1/tts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VOICELAB_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "hello-world-001",
},
body: JSON.stringify({
text: "Hello from VoiceLab.",
language: "en",
voice_id: "voice_01J9NEUTRAL0000000000000001",
speed: 1,
}),
});
if (!response.ok) throw new Error(await response.text());
await writeFile("hello.wav", Buffer.from(await response.arrayBuffer()));java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
var json = """
{"text":"Hello from VoiceLab.","language":"en",
"voice_id":"voice_01J9NEUTRAL0000000000000001","speed":1}
""";
var request = HttpRequest.newBuilder(URI.create("https://api.voicelab.uz/v1/tts"))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", "hello-world-001")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofByteArray());
Files.write(Path.of("hello.wav"), response.body());200 OK returns the audio bytes, not JSON.
http
Content-Type: audio/wav
X-VoiceLab-Characters-Used: 24
X-VoiceLab-Audio-Duration-Ms: 1180
X-VoiceLab-Sample-Rate: 24000One Unicode character uses one credit. VoiceLab releases reserved credits if generation fails.
Available languages and models
Return supported languages, models, formats, and sample rates.
Requires tts:read. Fetch this catalog instead of hardcoding language or model names.
json
{
"provider": "Lison-VoiceLab",
"sample_rate": 24000,
"data": [
{"code": "en", "name": "English", "native_name": "English"},
{"code": "uz", "name": "Uzbek", "native_name": "O'zbek"}
],
"models": [
{
"id": "lison",
"languages": ["en", "uz", "ru"],
"outputs": [{"format": "wav", "sample_rate": 24000, "streaming": false}]
}
]
}Idempotency
Keys must contain 8 to 128 letters, numbers, ., _, or - characters.
- Retry a timed-out request with the same key and the same JSON body.
- Use a new key when any input changes.
- A replay returns the stored WAV with
Idempotent-Replayed: true. - Reusing a key with different input returns
409 idempotency_key_reused.
List generations
List generated audio with cursor pagination.
bash
curl -fS 'https://api.voicelab.uz/v1/tts/generations?limit=30' \
-H "Authorization: Bearer $VOICELAB_API_KEY"python
import os
import requests
response = requests.get(
"https://api.voicelab.uz/v1/tts/generations",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
params={"limit": 30},
timeout=30,
)
response.raise_for_status()
print(response.json())ts
const response = await fetch(
"https://api.voicelab.uz/v1/tts/generations?limit=30",
{ headers: { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` } },
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class Main {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(
URI.create("https://api.voicelab.uz/v1/tts/generations?limit=30"))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}Requires tts:read. limit defaults to 30 and accepts 1 to 100.
json
{
"data": [
{
"id": "gen_01J...",
"text_preview": "Your order is ready.",
"voice": {"id": "voice_01J...", "name": "Lison Neutral", "language": "en"},
"duration_ms": 1180,
"created_at": "2026-08-16T12:00:00Z",
"audio_available": true
}
],
"next_cursor": null,
"request_id": "req_01J..."
}Get a generation
Read one generation and its short-lived audio URL.
bash
curl -fS 'https://api.voicelab.uz/v1/tts/generations/gen_01J...' \
-H "Authorization: Bearer $VOICELAB_API_KEY"python
import os
import requests
generation_id = "gen_01J..."
response = requests.get(
f"https://api.voicelab.uz/v1/tts/generations/{generation_id}",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
timeout=30,
)
response.raise_for_status()
print(response.json())ts
const generationId = "gen_01J...";
const response = await fetch(
`https://api.voicelab.uz/v1/tts/generations/${generationId}`,
{ headers: { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` } },
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class Main {
public static void main(String[] args) throws Exception {
var generationId = "gen_01J...";
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/tts/generations/" + generationId))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}Add ?download=1 to download the audio. Signed audio URLs expire after about 10 minutes and must not be exposed publicly.
json
{
"id": "gen_01J...",
"text": "Your order is ready.",
"voice": {"id": "voice_01J...", "name": "Lison Neutral", "language": "en"},
"speed": 1,
"duration_ms": 1180,
"sample_rate": 24000,
"audio_url": "https://storage.example/signed-url...",
"audio_url_expires_at": "2026-08-16T12:10:00Z",
"request_id": "req_01J..."
}Delete a generation
Delete one generation and its private audio.
bash
curl -fS -X DELETE \
'https://api.voicelab.uz/v1/tts/generations/gen_01J...' \
-H "Authorization: Bearer $VOICELAB_API_KEY"python
import os
import requests
generation_id = "gen_01J..."
response = requests.delete(
f"https://api.voicelab.uz/v1/tts/generations/{generation_id}",
headers={"Authorization": f"Bearer {os.environ['VOICELAB_API_KEY']}"},
timeout=30,
)
response.raise_for_status()
assert response.status_code == 204ts
const generationId = "gen_01J...";
const response = await fetch(
`https://api.voicelab.uz/v1/tts/generations/${generationId}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.VOICELAB_API_KEY}` },
},
);
if (!response.ok) throw new Error(await response.text());
if (response.status !== 204) throw new Error(`Unexpected status: ${response.status}`);java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class Main {
public static void main(String[] args) throws Exception {
var generationId = "gen_01J...";
var request = HttpRequest.newBuilder(URI.create(
"https://api.voicelab.uz/v1/tts/generations/" + generationId))
.header("Authorization", "Bearer " + System.getenv("VOICELAB_API_KEY"))
.DELETE()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.discarding());
if (response.statusCode() != 204) {
throw new RuntimeException("Unexpected status: " + response.statusCode());
}
}
}Requires tts:write and returns 204 No Content.
Realtime speech
Stream raw 24 kHz PCM16 audio over one WebSocket.
Server clients authenticate during the WebSocket upgrade with xvl-api-key. The key needs tts:realtime permission.
python
import json
import os
from pathlib import Path
import websocket
ws = websocket.create_connection(
"wss://api.voicelab.uz/v1/tts/stream",
header=[f"xvl-api-key: {os.environ['VOICELAB_API_KEY']}"],
timeout=30,
)
print(json.loads(ws.recv())) # ready
ws.send(json.dumps({
"text": "Hello from realtime VoiceLab.",
"language": "en",
"voice_id": "voice_01J9NEUTRAL0000000000000001",
"speed": 1,
}))
with Path("speech.pcm").open("wb") as output:
while True:
frame = ws.recv()
if isinstance(frame, bytes):
output.write(frame)
continue
event = json.loads(frame)
if event.get("event") in {"done", "error"}:
break
ws.close()ts
import { writeFile } from "node:fs/promises";
import WebSocket from "ws";
const apiKey = process.env.VOICELAB_API_KEY;
if (!apiKey) throw new Error("VOICELAB_API_KEY is not set");
const ws = new WebSocket("wss://api.voicelab.uz/v1/tts/stream", {
headers: { "xvl-api-key": apiKey },
});
const chunks: Buffer[] = [];
ws.on("message", async (data, isBinary) => {
if (isBinary) {
chunks.push(Buffer.from(data));
return;
}
const event = JSON.parse(data.toString());
if (event.event === "ready") {
ws.send(JSON.stringify({
text: "Hello from realtime VoiceLab.",
language: "en",
voice_id: "voice_01J9NEUTRAL0000000000000001",
speed: 1,
}));
}
if (event.event === "done") {
await writeFile("speech.pcm", Buffer.concat(chunks));
ws.close();
}
if (event.event === "error") throw new Error(event.message);
});java
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
public final class RealtimeTts {
public static void main(String[] args) throws Exception {
var audio = new ByteArrayOutputStream();
var finished = new CountDownLatch(1);
WebSocket.Listener listener = new WebSocket.Listener() {
private final StringBuilder text = new StringBuilder();
@Override public void onOpen(WebSocket socket) { socket.request(1); }
@Override public CompletionStage<?> onText(
WebSocket socket, CharSequence data, boolean last) {
text.append(data);
if (last) {
String event = text.toString();
text.setLength(0);
if (event.contains("\"ready\"")) {
socket.sendText("""
{"text":"Hello from realtime VoiceLab.","language":"en",
"voice_id":"voice_01J9NEUTRAL0000000000000001","speed":1}
""", true);
}
if (event.contains("\"done\"") || event.contains("\"error\"")) {
finished.countDown();
}
}
socket.request(1);
return null;
}
@Override public CompletionStage<?> onBinary(
WebSocket socket, ByteBuffer data, boolean last) {
byte[] chunk = new byte[data.remaining()];
data.get(chunk);
audio.writeBytes(chunk);
socket.request(1);
return null;
}
};
WebSocket socket = HttpClient.newHttpClient().newWebSocketBuilder()
.header("xvl-api-key", System.getenv("VOICELAB_API_KEY"))
.buildAsync(URI.create("wss://api.voicelab.uz/v1/tts/stream"), listener)
.join();
finished.await();
Files.write(Path.of("speech.pcm"), audio.toByteArray());
socket.sendClose(WebSocket.NORMAL_CLOSURE, "done").join();
}
}bash
wscat -c 'wss://api.voicelab.uz/v1/tts/stream' \
-H "xvl-api-key: $VOICELAB_API_KEY"
# Send after the ready event:
{"text":"Hello from realtime VoiceLab.","language":"en","voice_id":"voice_01J9NEUTRAL0000000000000001","speed":1}The server sends ready first:
json
{"event":"ready","format":"pcm_s16le","sample_rate":24000,"channels":1}Send one text request per connection. text accepts up to 8 KiB, and speed accepts 0.5 to 2.0. Binary frames contain signed 16-bit little-endian mono PCM. They are not WAV files. The final event is {"event":"done","duration_ms":1420}.
Send {"action":"interrupt"} to stop active synthesis. The server returns {"event":"interrupted"}.
Browser authentication
Browsers cannot set xvl-api-key on a WebSocket. Your backend must mint a short-lived ticket:
Mint a ticket for one browser WebSocket connection.
json
{"transport":"websocket","service":"tts"}Connect to wss://api.voicelab.uz/v1/tts/stream?ticket=<short-lived-ticket>. Tickets expire after about two minutes. Do not store them or include them in logs.
Realtime errors include invalid_message, validation_error, busy, not_busy, insufficient_credits, overloaded, timeout, service_unavailable, and generation_failed.
Errors
| Status | Code | Fix |
|---|---|---|
400 | invalid_json, invalid_idempotency_key | Fix the body or key |
401 | invalid_api_key | Send a valid API key |
402 | insufficient_credits | Add credits |
403 | insufficient_scope | Update key permissions or allowed IPs |
404 | voice_unavailable, generation_not_found | Refresh the voice or generation ID |
409 | idempotency_key_reused | Use a new key for changed input |
422 | validation_error | Correct fields listed in error.fields |
429 | rate_limited, overloaded | Wait for Retry-After, then retry |
500, 503 | internal_error, service_unavailable | Retry with the same idempotency key |