API Reference
Base URL https://api.vocallab.ai. Every request needs Authorization: Bearer vl_live_... (see API Keys).
GET /api/v1/ping
Connection + auth test. Returns your points balance.
{ "ok": true, "points": 12345, "unit": "points (1 pt ≈ 1 second of audio)" }
GET /api/v1/voices
Voices you can use — your own cloned and designed voices first, then the public catalog. Use a returned id as the voice for generation. Catalog voices also accept their slug.
| Query | Default | Notes |
|---|---|---|
limit | (none) | How many to return, 1–500. Omitted returns the whole list — around 260 voices |
offset | 0 | How many to skip, for paging |
q | — | Case-insensitive filter on the voice id, slug and name, e.g. ?q=ashley |
type | — | preset, clone or designed |
Every response carries total, count, offset and has_more, so you can page without guessing. The same four parameters (minus type) work on GET /api/v1/voices/clones and GET /api/v1/voices/designs.
curl "https://api.vocallab.ai/api/v1/voices?q=narrator&limit=20" \
-H "Authorization: Bearer vl_live_..."
{ "voices": [
{ "id": "Ashley", "slug": "warm-natural-female-explainer-voice-for-youtube-podcasts",
"name": "Warm Natural Female Explainer Voice for YouTube & Podcasts",
"type": "preset", "languages": ["en"], "language_code": "en-US",
"accent": "American", "category": "Narration" },
{ "id": "default-abc123__my-voice", "name": "My Narrator", "type": "clone", "languages": ["en-GB"] }
] }
languages is the bare language and flattens every accent together. language_code and accent (presets only) say which English or Portuguese a voice actually is — use them to pick a British or European Portuguese voice without guessing from the name.
GET /api/v1/models
The selectable speech models. Pass a key as model on POST /api/v1/tts (default v-pro). API keys can use all of them. See Voice Models for the differences.
{ "default": "v-pro", "models": [
{ "key": "v-studio", "label": "VocalLab Studio", "steerable": true, "costMultiplier": 1,
"supportsTemperature": false, "supportsDeliveryMode": true },
{ "key": "v-flash", "label": "VocalLab Studio Flash", "steerable": false, "costMultiplier": 0.75,
"supportsTemperature": false, "supportsDeliveryMode": false },
{ "key": "v-pro", "label": "VocalLab Pro", "steerable": false, "costMultiplier": 1,
"supportsTemperature": true, "supportsDeliveryMode": false },
{ "key": "v-lite", "label": "VocalLab Lite", "steerable": false, "costMultiplier": 0.5,
"supportsTemperature": true, "supportsDeliveryMode": false }
] }
supportsTemperature and supportsDeliveryMode tell you which expressiveness control a model actually honours — they do not overlap. Send the wrong one and it is rejected rather than silently ignored.
POST /api/v1/tts
Generate speech.
| Field | Required | Notes |
|---|---|---|
text | yes | Up to 2,000 characters. Each call is synthesized as a single request — for longer scripts, split the text and make multiple calls |
voice | yes | A voice id from /api/v1/voices |
model | no | v-studio (newest, steerable, 200+ languages), v-flash (200+ languages, ~5× faster, ¾ points), v-pro (default — highest fidelity, 15 languages), or v-lite (fast, ½ points). See GET /api/v1/models |
speed | no | Number 0.5–1.5 (step 0.05). Defaults to the voice preset |
temperature | no | Number 0.7–1.5 (step 0.05). Higher = more expressive & variable. Ignored by v-studio and v-flash — use delivery_mode on v-studio. Defaults to the voice preset |
delivery_mode | no | STABLE, BALANCED or CREATIVE — how much the model varies its performance. v-studio only; sending it with any other model returns 422 |
enhance_generation | no | true denoises the generated audio. Works on every model. Default false — it can leave a voice sounding over-smoothed |
format | no | One of MP3 (default), WAV, FLAC, OGG_OPUS, LINEAR16, PCM, ALAW, MULAW |
bit_rate | no | Integer 32000–320000. MP3 & OGG_OPUS only — ignored for other formats |
sample_rate | no | One of 8000, 16000, 22050, 24000, 32000, 44100, 48000 (Hz) |
captions | no | true (or "phrase" / "word") also returns the SRT subtitles inline as captions, so you don't need a second call. See GET /api/v1/tts/:id/captions |
curl -X POST https://api.vocallab.ai/api/v1/tts \
-H "Authorization: Bearer vl_live_..." \
-H "Content-Type: application/json" \
-d '{"text":"Hello from VocalLab","voice":"Ashley"}'
Returns the audio inline (base64) plus an id. The hosted URL appears once the upload finishes — poll GET /api/v1/tts/:id for it.
Each request is capped at 2,000 characters and maps to exactly one speech synthesis — there's no hidden splitting or fan-out, so cost and latency per call stay predictable. Requests are metered against your points balance and run through a fair-use queue, so heavy automation never blocks the app. To narrate a longer script, break it into ≤ 2,000-character pieces and send them as separate calls (respecting the 60 requests/minute rate limit).
{ "id": "...", "status": "pending", "audio_base64": "data:audio/mp3;base64,...",
"audio_url": null, "stream_url": "https://api.vocallab.ai/api/v1/tts/.../audio",
"captions_url": "https://api.vocallab.ai/api/v1/tts/.../captions",
"format": "MP3", "model": "v-pro", "points_used": 12 }
Model & cost.
v-flashbills at ¾ (points = ⌈ characters ÷ 20 ⌉) andv-liteat half (÷ 30) the points ofv-pro/v-studio(÷ 15).v-studiois the only model that follows expression/steering instructions — see Voice Models and Voice Steering.
Pauses
Insert a silence of an exact length anywhere in text with a self-closing <break /> tag:
curl -X POST https://api.vocallab.ai/api/v1/tts \
-H "Authorization: Bearer vl_live_..." \
-H "Content-Type: application/json" \
-d '{"text":"Let me think <break time=\"1.5s\" /> Yes, I have thought about it.","voice":"Ashley"}'
- Seconds or milliseconds —
<break time="1.5s" />and<break time="1500ms" />are identical. - Up to 20 break tags per request, in every supported language.
- Tag names are case-insensitive, but the tag must be well-formed and self-closing.
- Break tags count toward the 2,000-character limit and are stripped from captions/SRT.
- Softer pacing needs no tag: commas, periods, em dashes (—) and blank lines all shape delivery. See Voice Steering.
GET /api/v1/tts/:id
Generation status and the hosted audio URL once ready.
{ "id": "...", "status": "ready", "audio_url": "https://...",
"stream_url": "https://api.vocallab.ai/api/v1/tts/.../audio",
"captions_url": "https://api.vocallab.ai/api/v1/tts/.../captions", "format": "MP3" }
GET /api/v1/tts/:id/audio
Stream the finished clip's audio bytes directly — an alternative to the base64 audio_base64 blob. Served inline (plays in a browser or media player) and supports the HTTP Range header so clients can seek. Returns 409 while the clip is still finalizing — poll GET /api/v1/tts/:id until status is ready, then stream.
curl -L https://api.vocallab.ai/api/v1/tts/GENERATION_ID/audio \
-H "Authorization: Bearer vl_live_..." \
-o speech.mp3
The same link is returned as stream_url on POST /api/v1/tts and GET /api/v1/tts/:id.
GET /api/v1/tts/:id/captions
Subtitles for a generation, as an .srt file. Cues are timed from the actual speech (word-level timings returned with the synthesis), and any [emotion] markup or <break /> pause tag is stripped out, so only spoken words reach the file.
Captions read from stored timings, not from the audio file — they're available immediately, even while status is still pending.
| Query | Default | Notes |
|---|---|---|
format | srt | srt returns the subtitle file; json returns { srt, words, duration } for custom rendering |
mode | phrase | phrase = readable subtitle lines (split on pauses, sentence and clause ends). word = one cue per word, for karaoke-style highlighting |
max_words | 4 | Words per line in phrase mode, 2–7 |
uppercase | false | true UPPERCASES the caption text |
curl -L "https://api.vocallab.ai/api/v1/tts/GENERATION_ID/captions" \
-H "Authorization: Bearer vl_live_..." \
-o speech.srt
1
00:00:00,000 --> 00:00:01,240
Hello from VocalLab,
2
00:00:01,240 --> 00:00:03,100
this is your narrator speaking.
With format=json:
{ "id": "...", "mode": "phrase", "duration": 3.1,
"srt": "1\n00:00:00,000 --> 00:00:01,240\nHello from VocalLab,\n\n...",
"words": [ { "word": "Hello", "start": 0, "end": 0.32 } ] }
The same link is returned as captions_url on POST /api/v1/tts and GET /api/v1/tts/:id. To get the SRT back with the audio in a single call, send "captions": true on POST /api/v1/tts.
Returns 409 captions_unavailable when a generation has no stored word timings (a few older clips) — the audio is unaffected.
DELETE /api/v1/tts/:id
Permanently delete a generation you own — both the stored audio file and its database record. Handy for cleaning up after a download or honoring a data-deletion request. An id that doesn't exist, or isn't yours, returns 404.
curl -X DELETE https://api.vocallab.ai/api/v1/tts/GENERATION_ID \
-H "Authorization: Bearer vl_live_..."
{ "ok": true, "id": "...", "deleted": true }
GET /api/v1/voices/languages
The languages and recording limits that apply to voice cloning and voice design. Read this instead of hardcoding the list — it's the same source of truth the Studio upload form uses.
Over 470 languages are returned, each with any accent variants it offers. code is a BCP-47 tag; pass either the language's own code or one of its accent codes as language when you clone or design.
experimental: true means the language has no stock voice catalog behind it — cloning and design still work, but quality varies more.
{ "languages": [
{ "name": "Auto-detect", "code": "auto", "experimental": true, "accents": [] },
{ "name": "English", "code": "en", "experimental": false, "accents": [
{ "name": "American", "code": "en-US" },
{ "name": "British", "code": "en-GB" },
{ "name": "Indian", "code": "en-IN" }
] },
{ "name": "Portuguese", "code": "pt", "experimental": false, "accents": [
{ "name": "Brazilian", "code": "pt-BR" },
{ "name": "European", "code": "pt-PT" }
] }
],
"sample_limits": { "max_samples": 1, "max_base64_length": 4194304,
"allowed_formats": ["mp3", "wav", "webm"] } }
Changed:
codeused to be an enum of 16 values (EN_US,PT_BR, …) with anOtherentry meaning "let the engine guess". Those old values are still accepted wherever alanguageis taken, so existing integrations keep working — but the list they came from is now the full set, andOtheris the explicitautoentry.
Voice design
Invent a brand-new voice from a written description — no recording needed. It's two calls: generate candidate previews, then save the one you like.
POST /api/v1/voices/designs/previews
Generates up to three candidate voices. Billed in points — each preview is a real synthesis of preview_text at the API rate (⌈ characters ÷ 15 ⌉ per preview).
Nothing is saved yet, so this doesn't use a design slot — but it does require a free one. With your designs already at the plan limit the call returns 409 design_limit and no points are spent, because a voice you couldn't save isn't worth paying for.
| Field | Required | Notes |
|---|---|---|
prompt | yes | Describe the voice in English, 30–250 characters: gender, age, accent, tone, pace and delivery |
language | no | Language the voice should speak (English, ES_ES, …). Defaults to auto — inferred from the prompt |
preview_text | no | What the previews say, up to 1,000 characters. Defaults to a short sample line |
count | no | How many candidates to generate, 1–3 (default 3). Each one is billed |
include_audio | no | true also returns each preview as base64. Every preview already has a preview_url, so the bytes are opt-in |
curl -X POST https://api.vocallab.ai/api/v1/voices/designs/previews \
-H "Authorization: Bearer vl_live_..." \
-H "Content-Type: application/json" \
-d '{"prompt":"A warm, unhurried British woman in her forties, low and reassuring, like a documentary narrator.","count":2}'
{ "previews": [
{ "preview_id": "...", "text": "Hello! This is a preview of my voice...",
"preview_url": "https://api.vocallab.ai/api/v1/audio/..." }
],
"points_used": 12, "expires_in": 1800 }
A preview_id is not a usable voice yet, and it's valid for about 30 minutes.
Previews you don't keep are thrown away. Save the one you want before generating another set — calling this endpoint again discards your previous previews and their preview_urls stop working, and saving one discards the rest of its batch. Nothing is left behind either way.
POST /api/v1/voices/designs
Keeps a preview as a permanent voice. Costs no points (the previews were already billed) but uses one of your plan's design slots. The returned id works immediately as the voice on POST /api/v1/tts.
| Field | Required | Notes |
|---|---|---|
preview_id | yes | A preview_id from the previews call, within 30 minutes |
name | yes | Display name, up to 80 characters |
description | no | Free-text note, up to 500 characters |
tags | no | Up to 10 labels |
sample_base64 | no | Audition clip to store with the voice. Not needed inside the 30-minute window — the preview you chose is kept automatically |
{ "id": "default-abc123__narrator", "name": "Documentary Narrator", "type": "designed",
"languages": ["en-GB"], "created_at": "...",
"preview_url": "https://api.vocallab.ai/api/v1/audio/...", "used": 3, "limit": 20 }
GET /api/v1/voices/designs
Your designed voices, newest first, with slot usage: { "voices": [...], "used": 3, "limit": 20 }.
DELETE /api/v1/voices/designs/:voiceId
Deletes the voice from the provider and from your library. On a paid plan this frees a slot. Returns { "ok": true, "id": "...", "deleted": true }.
Voice cloning
Recreate a specific voice from one short recording. Cloning costs no points — it's limited by the number of clone slots on your plan. Only clone a voice you own or have written permission to use.
POST /api/v1/voices/clones
| Field | Required | Notes |
|---|---|---|
name | yes | Display name, up to 80 characters |
language | yes | A name or code from GET /api/v1/voices/languages — a language (en), an accent (en-GB), or a legacy enum (EN_US). Use auto to have it detected |
samples | yes | [{ "audio_base64": "...", "transcript": "..." }] — one recording of about 30 seconds. transcript is optional but noticeably improves the clone |
remove_background_noise | no | Clean the recording first. Default true |
Almost any audio file works. audio_base64 is the raw base64 of your recording, with no data: prefix — MP3, WAV, M4A, AAC, OGG, FLAC, WebM, the audio track of an MP4, and more. Anything the speech provider wouldn't accept directly is converted server-side to mono 16-bit WAV, exactly as the web apps do in the browser before uploading, so the API is no fussier than the Studio.
The conversion also:
- trims to the first 30 seconds — a longer file is fine to send, only the opening is used;
- down-mixes stereo to mono and resamples to 32 kHz;
- shrinks oversized files, so a big 24-bit studio WAV no longer has to be prepared by hand.
The limit is on the base64 string, not the decoded bytes: up to 8,000,000 characters, roughly a 6 MB file. Anything larger returns 413 sample_too_large; a file with no readable audio returns 400 unreadable_audio.
When anything was changed, the response includes an audio_notes array saying so — worth logging, so a clone built from the first 30 seconds of a long recording is never a silent surprise:
{ "id": "...", "name": "My Narrator", "type": "clone", "used": 2, "limit": 100,
"audio_notes": ["Re-encoded to mono 16-bit WAV at 32 kHz — aac isn't a format the provider accepts.",
"Trimmed to the first 30s (the sample was 90s)."] }
curl -X POST https://api.vocallab.ai/api/v1/voices/clones \
-H "Authorization: Bearer vl_live_..." \
-H "Content-Type: application/json" \
-d "{\"name\":\"My Narrator\",\"language\":\"English\",\"samples\":[{\"audio_base64\":\"$(base64 -w0 sample.mp3)\"}]}"
{ "id": "default-abc123__my-narrator", "name": "My Narrator", "type": "clone",
"languages": ["en-GB"], "created_at": "...", "used": 2, "limit": 100 }
GET /api/v1/voices/clones
Your cloned voices, newest first, with slot usage: { "voices": [...], "used": 2, "limit": 100 }.
DELETE /api/v1/voices/clones/:voiceId
Deletes the voice from the provider and from your library. On a paid plan this frees a slot. Returns { "ok": true, "id": "...", "deleted": true }.
Slots are separate. Cloned and designed voices never share a budget — see Plans & Limits for the per-plan numbers. On the Free plan the cap is a lifetime one: deleting a voice does not free the slot. In a workspace, members work within the owner's plan limits, and design points come out of the owner's balance.
What costs points
| Points | |
|---|---|
| Cloning a voice | none — limited by clone slots only |
| Listing or deleting a voice | none |
| Designing a voice (previews) | count × ⌈ preview_text ÷ 15 ⌉ — every preview is a real synthesis |
| Saving a designed voice | none — the previews were already billed |
GET /api/v1/me
Your points balance and plan.
Errors
| Status | Meaning |
|---|---|
400 | A required field is missing from the body (missing_text, missing_voice, missing_name, missing_prompt, …) |
401 | Missing or invalid API key |
402 | Not enough points |
403 | Plan doesn't include API access (requires Pro or higher) |
404 | Generation or voice not found (or not yours) |
409 | Audio not ready yet — poll until ready; no word timings stored (captions); or every clone/design slot is in use (clone_limit, design_limit) |
413 | Text, preview text, or audio sample too long |
422 | Unknown model, unsupported language, or an out-of-range parameter |
429 | Rate limit reached — 60 requests / minute per key, and separately 60 cloning or voice-design calls per hour |
502 | The speech provider failed or rejected the request — the message says why, and retryable says whether to try again |
Errors are shaped { "error": { "code": "...", "message": "..." } }.
Pricing
Generations are billed from your points balance, calculated from the text length (not the final audio duration):
points = ⌈ characters ÷ 15 ⌉ (API rate — about 15 characters per point)
⌈ ⌉ rounds up to the next whole point. The web app uses 17 characters per point, so the API carries a small (~13%) premium. Points are metered before the upstream call — a request that would exceed your balance returns 402 and is never generated. Each response includes the exact points_used. Because speech averages ~15–17 characters/second, 1 point ≈ 1 second of audio, but the exact charge always follows the formula above. See Credits & Minutes for more.


