Docs / API

The VoiceAct API

Everything the Studio does with a script, over HTTP: 50 native voices in 10 languages, Voice Design, cloning, your saved voices, timing marks, word timings and subtitles, live audio, and files in MP3, WAV or 8 kHz telephone formats. It also speaks OpenAI's speech format, so existing code can switch with two lines. You send text, get a job back, and download or stream the audio. Base URL: https://api.voiceact.ai/v1

Keys work on every plan, the free one included. API jobs use the same characters and limits as the Studio and show up in your history. The machine-readable description is at /v1/openapi.json.

Quick start

Create a key on your dashboard, then make a short line and wait for it. With wait the answer comes back finished for short texts; without it you get the job at once and poll.

curl
export VOICEACT_API_KEY=va_...   # from voiceact.ai/dashboard/api

curl https://api.voiceact.ai/v1/speech \
  -H "Authorization: Bearer $VOICEACT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Welcome to the show.", "voice": "en_grace_soft_narrator", "wait": 30}'

The answer is the job. When status is completed, download the file (replace JOB_ID with its id):

curl
curl -L https://api.voiceact.ai/v1/speech/JOB_ID/audio \
  -H "Authorization: Bearer $VOICEACT_API_KEY" -o welcome.mp3

Python

python
import os, time, requests

BASE = "https://api.voiceact.ai/v1"
H = {"Authorization": f"Bearer {os.environ['VOICEACT_API_KEY']}"}

job = requests.post(f"{BASE}/speech", headers=H, json={
    "text": "Willkommen bei unserem Kundenservice.",
    "voice": "de_jonas_newscaster",          # German voice, so German by default
    "format": "wav",
}).json()

while job["status"] in ("queued", "processing"):
    time.sleep(2)
    job = requests.get(f"{BASE}/speech/{job['id']}", headers=H).json()

if job["status"] == "failed":
    raise RuntimeError(job["error"]["message"])   # failed jobs aren't charged

audio = requests.get(job["audio"]["url"], headers=H)
open("welcome.wav", "wb").write(audio.content)

Node.js

javascript
import { writeFile } from "node:fs/promises";

const H = { Authorization: `Bearer ${process.env.VOICEACT_API_KEY}` };

const res = await fetch("https://api.voiceact.ai/v1/speech", {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({ text: "Your order has shipped.", voice: "en_grace_soft_narrator", wait: 60 }),
});
const job = await res.json();
if (!res.ok) throw new Error(job.error.message);
if (job.status !== "completed") throw new Error(`still ${job.status}: poll GET /v1/speech/${job.id}`);

const audio = await fetch(job.audio.url, { headers: H });
await writeFile("shipped.mp3", Buffer.from(await audio.arrayBuffer()));

Authentication

Send your key in the Authorization header: Bearer va_…. A key acts as your account. Keep it on your server: anyone who has it can spend your characters. If one leaks, revoke it on the dashboard; it stops working at once. You can have 10 active keys, so give each server or project its own.

The API doesn't answer browsers on other websites (no CORS), which keeps keys out of web pages. Call it from your backend.

Create speech

POST /v1/speech with a JSON body. Give either voice or voice_description.

FieldWhat it does
textWhat to say. Timing marks work as in the Studio (see below). The longest text per request depends on your plan.
voiceA voice id from GET /v1/voices: a preset like en_grace_soft_narrator, or one of your saved voices.
voice_descriptionVoice Design: describe a voice instead, up to 500 characters. Real people can't be imitated.
languageA name (German), a code (de) or auto to detect it from the text. Default: the voice's own language, else English.
formatThe format the job's audio link points to (default mp3). Every format stays downloadable.
waitWait up to this many seconds (0–60) for the audio. Finished in time: 200 with the finished job. Otherwise 202 and you poll.

Unknown fields are an error, so a typo in a field name doesn't silently do nothing.

Retries without double charges

Send an Idempotency-Key header (any string up to 100 characters, such as your own order id). If the same key arrives again within 24 hours with the same body, you get the first job back instead of a second one, marked with Idempotent-Replayed: true. The same key with a different body is a 422.

The job

GET /v1/speech/{id} returns the job. status goes from queued to processing to completed or failed. Generation runs at about twice real time once a job starts, so a minute of speech takes roughly 30 seconds.

json
{
  "id": "3f6c1e0a-9b1d-4d2e-8a51-0c2f6d0e7b11",
  "object": "speech",
  "status": "completed",
  "created_at": "2026-10-28T20:14:03.512Z",
  "completed_at": "2026-10-28T20:14:06.020Z",
  "language": "English",
  "voice": { "id": "en_grace_soft_narrator", "type": "preset", "description": null },
  "text_length": 20,
  "characters": 25,
  "duration_seconds": 1.62,
  "queue_position": null,
  "audio": { "format": "mp3", "url": "https://api.voiceact.ai/v1/speech/3f6c…/audio?format=mp3" },
  "words": null,
  "words_ready": true,
  "speech_check": "passed",
  "error": null,
  "source": "api"
}

characters is what the job costs: an estimate until it finishes, then the amount charged. speech_check comes from our listener: every take is transcribed and compared with your text. flagged means it may contain a misread word; it shows up shortly after completion, and stays null until then. queue_position counts your own jobs ahead of this one.

A flagged job can be made again once for free within 7 days: POST /v1/speech/{id}/regenerate when free_retake_available is true. You get a new job with the same text and voice; its retake_of points to the first one.

GET /v1/speech lists your jobs, newest first, with limit (up to 100), cursor (the next_cursor of the previous page) and source (api, studio or all).

Download audio

GET /v1/speech/{id}/audio?format=… returns the file. Each format is made from the 24 kHz master the first time you ask for it.

formatFileFor
mp3MP3, 128 kbps, 24 kHz monoweb, apps, video
wavWAV, 16-bit, 24 kHz mono (the master)editing, highest quality
wav_16000WAV, 16-bit, 16 kHzwideband telephony, speech tools
wav_8000WAV, 16-bit, 8 kHzphone systems that want linear PCM (e.g. Asterisk)
ulaw_8000WAV, G.711 µ-law, 8 kHztelephony in North America and Japan
alaw_8000WAV, G.711 A-law, 8 kHztelephony in Europe and most other regions

The 8 and 16 kHz files are filtered before they're downsampled, so nothing above the new limit folds back into the speech band. Every file carries a signed C2PA manifest that marks it as AI-generated, and the 24 kHz files also carry an inaudible watermark. The watermark doesn't survive 8 kHz audio (we measured it), so telephone files rely on the manifest.

Live audio

GET /v1/speech/{id}/stream sends the audio while it's being made. Open it right after creating the job: it replays from the start, so nothing is lost if you connect late. In our tests the first audio arrives about 1.5 seconds after the request, about half a second of which is the model itself. For a finished job you get the stored file in the same format.

curl + ffplay
# Live audio while it generates: raw 16-bit PCM, 24 kHz mono
curl -sN "https://api.voiceact.ai/v1/speech/JOB_ID/stream" \
  -H "Authorization: Bearer $VOICEACT_API_KEY" \
  | ffplay -f s16le -ar 24000 -nodisp -autoexit -

format picks the samples: pcm_24000 (default), pcm_16000, pcm_8000 (16-bit little-endian), ulaw_8000 or alaw_8000. The response headers X-Audio-Format and X-Sample-Rate repeat what you get.

python, into a phone call
job = requests.post(f"{BASE}/speech", headers=H, json={
    "text": "Thanks for calling. Your balance is twelve euros and forty cents.",
    "voice": "en_grace_soft_narrator",
}).json()

# Open the stream right away: it replays from the start, so nothing is missed
with requests.get(f"{BASE}/speech/{job['id']}/stream", headers=H,
                  params={"format": "ulaw_8000"}, stream=True, timeout=120) as r:
    r.raise_for_status()                 # errors before the first audio are JSON
    for chunk in r.iter_content(1600):   # 0.2 s of 8 kHz µ-law
        send_to_call(chunk)              # your telephony code

The raw stream starts with the first audio, so an error before that (a failed job) is a normal JSON error. If a job hasn't started after 90 seconds you get 503 still_queued; open the stream again. A failure after audio has started ends the transfer early, and your HTTP client reports an incomplete response. Check the job afterwards to be sure it completed.

If you'd rather have explicit events, add transport=sse: Server-Sent Events with base64 audio, keepalive comments while the job waits, and a final end or error event.

text/event-stream
event: start
data: {"format":"pcm_24000","sample_rate":24000,"encoding":"pcm_s16le","channels":1}

event: audio
data: {"audio":"<base64 PCM>"}

: keepalive

event: end
data: {"duration_seconds":4.2,"live":true}

If the server retries a failed part of a job, the audio you already have is skipped. If the retry would change audio you already received, the stream stops with stream_interrupted instead of repeating it; the job carries on and the file is complete. The stream ends a moment before the job is marked completed, so check the job before downloading the file. Up to 10 streams can be open per account.

Voices

GET /v1/voices lists the presets and your saved voices, with ?language=de to see the voices native to one language. Every voice can speak all 10 languages, but a native voice sounds right in its own: an English voice speaking German keeps an English accent. The ten are English, German, French, Spanish, Italian, Portuguese, Russian, Chinese, Japanese and Korean.

voice_description in a speech request makes a new voice every time. For the same voice every time, design it and keep it, or clone one; voices you saved in the Studio work too. GET /v1/voices/{id} shows one voice, PATCH renames a saved voice ({"name": "…"}) and DELETE removes it, with its recording for a clone. How many voices you can keep depends on your plan.

Design a voice

Describe a voice and get three short takes to choose from, the same as casting in the Studio. Each take is measured against your description (gender, adult, pitch, pace) and the best match is marked. Keep one, and it becomes a saved voice that sounds the same in every job. A design costs a flat 100 characters, refunded if every take fails, and is kept for 24 hours.

curl
# 1. Three short takes of a described voice (100 characters)
curl https://api.voiceact.ai/v1/voices/design \
  -H "Authorization: Bearer $VOICEACT_API_KEY" -H "Content-Type: application/json" \
  -d '{"description": "warm, low female voice, calm and unhurried", "wait": 30}'

# 2. Listen to takes[].audio_url, then keep the one you like
curl https://api.voiceact.ai/v1/voices/design/DESIGN_ID/save \
  -H "Authorization: Bearer $VOICEACT_API_KEY" -H "Content-Type: application/json" \
  -d '{"take": 1, "name": "Narrator"}'
# → {"id": "VOICE_ID", "type": "saved", "kind": "designed", ...}: use it as "voice"

With text the takes speak the opening of your script, if it's short enough; otherwise a standard sentence in the language. Real people can't be imitated.

Clone a voice

Upload 10 to 60 seconds of one speaker (WAV, MP3, FLAC, OGG or M4A, up to 10 MB), with a transcript if you have one. We check the recording first for noise, echo and a second voice, and the voice is ready when the request returns. Cloning needs a paid plan or any purchase.

Every clone needs the speaker's permission. With each recording you confirm our consent statement: send consent=true and the statement's current consent_version from GET /v1/voices/clone/consent. If we change the statement, the old version stops working (409 consent_outdated) until you've read the new one. We keep a record of each confirmation.

curl
# The statement you confirm, and its version
curl https://api.voiceact.ai/v1/voices/clone/consent -H "Authorization: Bearer $VOICEACT_API_KEY"

curl https://api.voiceact.ai/v1/voices/clone \
  -H "Authorization: Bearer $VOICEACT_API_KEY" \
  -F file=@speaker.wav \
  -F name="Support line voice" \
  -F consent=true \
  -F consent_version=2026-10-18 \
  -F transcript="Thanks for calling. How can I help you today?"

Cloning through the API is new. If you get 403 cloning_not_available, clone in the Studio and use the saved voice's id.

OpenAI-compatible

Code written for OpenAI's speech endpoint works with VoiceAct by changing two things: the base URL and the key. POST /v1/audio/speech takes the same body and returns the audio itself.

python (openai SDK)
from openai import OpenAI

client = OpenAI(base_url="https://api.voiceact.ai/v1", api_key=os.environ["VOICEACT_API_KEY"])

with client.audio.speech.with_streaming_response.create(
    model="tts-1",                 # accepted, ignored
    voice="nova",                  # or any VoiceAct voice id
    input="Your order has shipped.",
) as response:
    response.stream_to_file("shipped.mp3")

OpenAI's voice names map to similar English presets: alloy Claire, ash Felix, ballad Omar, coral Sophia, echo Ben, fable Arthur, nova Amara, onyx James, sage Elena, shimmer Grace and verse Kai. Any VoiceAct voice id works as well, and the language is detected from the text (or set it with a language field).

response_format can be mp3, wav or pcm. pcm (24 kHz, 16-bit mono, the same as OpenAI's) streams while it's made; MP3 and WAV come when the job is done, for texts that take up to about 90 seconds. Longer texts: use POST /v1/speech. model is accepted and ignored; speed and instructions aren't supported, and the X-VoiceAct-Ignored header says when one was ignored.

Timing, words and subtitles

Timing marks go in the text: [pause 1.5s] adds silence, and [at 0:12.5] starts the next line at that time in the file. They aren't spoken and aren't billed. The timing guide has the details.

Word timings arrive a few seconds after the audio. Ask for them with GET /v1/speech/{id}?include=words once words_ready is true. Subtitles built from them: GET /v1/speech/{id}/subtitles?format=srt (or vtt).

Errors

Errors share one shape. Quote the request_id (also in the X-Request-Id header) when you write to us.

json
{
  "error": {
    "code": "insufficient_characters",
    "message": "Not enough characters left: this generation needs 1,240, you have 310.",
    "required": 1240,
    "available": 310,
    "request_id": "req_6a1f0c2e9b8d4f7a1c3e5b20"
  }
}
HTTPcodeMeaning
401missing_api_key, invalid_api_keyNo key, a malformed header, or a revoked key
400text_too_longOver your plan's per-request limit (limit says how much)
402insufficient_charactersNot enough characters left; top up or wait for the monthly refill
403clone_requires_purchase, cloning_not_available, voice_limit_reached, preset_voiceCloned voices need a paid plan or a purchase; API cloning is off; your plan's saved-voice limit; presets can't be changed
404voice_not_found, not_foundNo such voice, or no such job on your account
409not_ready, voice_not_ready, take_not_ready, job_running, generation_failed, retake_not_available, name_taken, consent_outdatedTry again later, the job failed, or the request conflicts with what's there
422invalid_request, nothing_to_speak, voice_description_refused, consent_required, idempotency_key_reusedSomething in the request; errors lists each field
429rate_limited, too_many_streamsSlow down; Retry-After says for how long
503 / 504service_busy, still_queued, api_disabled, still_generatingBusy or paused; try again after Retry-After. 504: the OpenAI-compatible call ran past 90 s, the job continues
500internal_errorOur fault; tell us the request_id

Limits and billing

You pay in characters, from the same balance as the Studio (prices). Chinese, Japanese and Korean characters count three times, timing marks don't count, and a failed job costs nothing. GET /v1/account shows your plan, the characters left and your limits.

FreeStarterCreatorStudio
Characters per request1,0003,00010,00020,000
Jobs per minute5101010
Jobs running at once1222

More jobs than that wait in your own queue; they aren't rejected. The limits are shared with the Studio. Every account can also make 300 API requests a minute in total, polling included (X-RateLimit-Remaining shows what's left). Free accounts get 3,000 characters a month. When it's very busy, free jobs can be turned away with a 503 while paid jobs always queue.

Need more per minute, bigger jobs, or invoices for a company? Write to support@voiceact.ai.

Your data

Texts and audio are processed in the EU: our servers are in Finland, and the GPUs are in Austria or rented in the EU. We don't train models on your texts or audio. Jobs stay in your history until you delete them, in the Studio or with DELETE /v1/speech/{id}, which removes the job and every file made from it. Details are in the privacy policy.

Not there yet

There are no webhooks yet, so poll or stream. There are no SDKs either: the API is plain HTTP and JSON, and the examples above are the whole client. Live audio goes through the same queue as everything else, so it isn't built for answering a caller within a few hundred milliseconds.

Using the audio in your own product, calls or videos is fine. If you want to build VoiceAct into a product where your customers make speech themselves, that counts as reselling access under our terms: write to us first and we'll sort it out.

API version 1, October 2026.