Evom Labs API Reference

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

POST/api/v1/voices

Creates a custom voice, and issues a new voice key where appropriate.

DELETE/api/v1/voices/{id}

Deletes an account-owned custom voice and its bound voice key.

POST/api/v1/tts/generate

Generates one finished audio file from text.

POST/api/v1/tts/bytes

Generates one finished audio file and returns its binary bytes.

POST/api/v1/tts/sse

Streams PCM16 audio events over a POST response.

POST/api/v1/tts/stream-token

Issues a short-lived token that opens the streaming WebSocket.

WS/ws/tts/stream?token=<stream_token>

Delivers PCM16 audio as binary frames.

GET/api/v1/usage

Reads the allowance of the account behind the voice key.

GET/api/v1/keys

Lists account-owned Admin, TTS and STT API-key metadata with remaining usage.

Needs verification: public exposure

The voice enrollment route and the public WebSocket origin have to be checked against the deployment configuration before either is treated as stable. The stream-token response returns the socket URL to use; prefer it over any hard-coded origin.

Voice enrollment

POST/api/v1/voices

Creates 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

FieldTypeRequiredDefaultDescription
filefileYes-Audio file, up to 10 MiB.
namestringYes-Voice name, not empty, up to 64 characters.
consentstringYes-Must be true. Confirms the speaker authorised the clone.
descriptionstringNo-Free-text note stored with the voice.
genderstringNofemaleOne of male, female or other.

Accepted when the MIME type starts with audio/, or when the extension is one of:

m4amp3wavwaveaacoggogaopusflacwebaaiffaifaifccafwmaamrmp4m4b

Consent is required

Enrollment is only for a voice whose speaker authorised the clone. The consent field records that authorisation and the request fails without it.

Examples

curl
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

JSON
{
  "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

Audio is deduplicated within the account. Re-uploading the same content returns 200 with deduped set to true. If the existing voice already has an active key, key_status is exists and api_key is null, because the endpoint never returns the secret of a key that already exists.

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

DELETE/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
curl -X DELETE https://studio.evomlabs.com/api/v1/voices/VOICE_ID   -H "Authorization: Bearer vc_ak_live_YOUR_KEY"

Response

JSON
{
  "ok": true,
  "data": {
    "voice_id": "VOICE_ID",
    "name": "Authorized sample voice",
    "deleted": true
  }
}

Needs verification

Deletion cannot be undone. Confirm the voice identifier before sending the request.

API keys and usage

GET/api/v1/keys

Returns 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

This endpoint never returns plaintext keys, encrypted secrets or hashes. It returns identifiers, prefixes, last-four characters, scopes, status and usage metadata only.
curl
curl https://studio.evomlabs.com/api/v1/keys   -H "Authorization: Bearer vc_ak_live_YOUR_KEY"

Response

JSON
{
  "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":[]}
  }
}
FieldTypeRequiredDescription
account_usageobjectYesPlan allowance, split into tts and stt. Each includes limit, used, remaining and unlimited. Reset metadata is returned alongside them.
keysobjectYesAlways contains admin, tts and stt arrays. An array can be empty.

Per-key usage fields

FieldTypeRequiredDescription
credit_limitinteger | nullYesPositive lifetime ceiling for this key, or null when this key has no separate ceiling.
credit_usedintegerYesCharacters used by this key over its lifetime.
credit_remaininginteger | nullYesmax(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.

allminecustom

Batch generation

POST/api/v1/tts/generate

Waits 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

FieldTypeRequiredDefaultDescription
textstringYes-Text to speak, not empty, up to 5,000 characters.
voice_idstringNo-Required with an account key. Omit with a voice key; if provided, it must match the key-bound voice.
languagestringNoautoauto, or one code or full name from the 646-language catalog.
formatstringNomp3Exactly mp3 or wav. The returned container and MIME match this value.
speednumberNo1.0Playback rate, clamped to 0.5 to 1.5.
cfg_valuenumberNo2.0How closely to follow the reference voice. A finite number from 0.0 to 4.0, including 0.
dit_stepsintegerNo10Diffusion 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.

FieldTypeDefaultDescription
do_normalizebooleanfalseText normalisation before synthesis.
denoisebooleanfalseDenoising, on the public batch route.
control_instructionstring""Not used to select a voice other than the one bound to the key.
use_prompt_textbooleanfalseAdvanced compatibility field.
prompt_textstring""Advanced compatibility field.
preprocess_promptbooleantrueReference prompt preprocessing.
postprocess_outputbooleantrueGenerated audio postprocessing.

Examples

curl
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
  }'
Python
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"])
JavaScript
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

JSON
{
  "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
  }
}

Loly 3.5 language catalog

Audio bytes

POST/api/v1/tts/bytes

Uses 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
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
HeaderValueMeaning
Content-Typeaudio/wav | audio/mpegWAV for wav, MPEG audio for mp3.
Content-Dispositionattachment filenameSuggested oriagent.wav or oriagent.mp3 filename.
X-OriAgent-Formatwav | mp3The selected output format.
X-OriAgent-Sample-Rateinteger HzPresent only when the backend provides it.
X-OriAgent-Chars-DeductedintegerExactly text.length charged for the request.

SSE audio stream

POST/api/v1/tts/sse

Uses 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
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

text/event-stream
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

Exactly one start event comes first, then one or more audio events in increasing sequence order, then one done event. An error event has code GENERATION_FAILED and replaces done if generation fails after the stream begins.

Stream token

POST/api/v1/tts/stream-token

Exchanges 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.

FieldTypeRequiredDescription
text_lengthintegerYesInteger from 1 through 10,000; allowance is deducted when the token is issued.
voice_idstringNoRequired with an account key. Omit with a voice key; if provided, it must match the key-bound voice.
curl
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

JSON
{
  "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

Open the socket right away. The text sent in the start message must not exceed the 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

The stream-token response carries the socket URL. Use that value rather than a hard-coded origin, and confirm with the operator that it resolves to a public wss endpoint before you ship an integration.

WebSocket

WS/ws/tts/stream?token=<stream_token>

Streams PCM16 mono at 24 kHz, in frames of roughly 200 ms.

Start message

JSON
{
  "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
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be start.
textstringYes-Must not exceed the max_length carried by the token.
languagestringNoautoauto, or one code or full name from the 646-language catalog.
speednumberNo1.0Clamped to 0.5 to 1.5.
cfg_valuenumberNo2.0Range 0.0 to 4.0.
dit_stepsintegerNo8Range 0 to 64.
control_instructionstringNo""Ignored while the stream uses a cloned-voice prompt.
use_prompt_textbooleanNofalseAdvanced compatibility field.
prompt_textstringNo""Advanced compatibility field.
do_normalizebooleanNofalseText normalisation before synthesis.
denoisebooleanNotrueDenoising, on the public batch route.
preprocess_promptbooleanNotrueReference prompt preprocessing.
postprocess_outputbooleanNotrueOutput postprocessing.

Important

The voice is locked into the stream token. The start message cannot change it.

Server events

JSON
// 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 client must tell the two apart: binary frames are raw PCM16 with no WAV header, JSON text frames carry the events. To write a .wav you have to add the header yourself, or fetch the audio_url from the done event, which may be a relative path. Do not hard-code the byte size of a frame.

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

Python
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")

Error codes

Evom Labs API documentation