Reference
API documentation
One endpoint does the work. Send text, get back audio URLs you can play, download, or host anywhere. Base URL: https://audio.chanovalabs.com
Authentication
Create a key in the dashboard and send it as a bearer token. Keys are shown once — store yours in an environment variable, never in client-side code.
Authorization: Bearer sk_live_…Quickstart
Request
curl https://audio.chanovalabs.com/v1/speech \
-H "Authorization: Bearer $CHANOVA_AUDIO_KEY" \
-H "Content-Type: application/json" \
-d '{"tok-key">"text": "Hello from ChanovaLabs Audio.", "tok-key">"voice": "af_heart"}'Response
{
"tok-key">"id": "6a7412ab9f3f731e4657b58c",
"tok-key">"status": "done",
"tok-key">"voice": "af_heart",
"tok-key">"characters": 28,
"tok-key">"cached": false,
"tok-key">"parts": 1,
"tok-key">"total_parts": 1,
"tok-key">"duration_seconds": 3,
"tok-key">"audio_urls": ["https://…/speech/ab/abcd…/part-0.wav"],
"tok-key">"credits_remaining": 99972
}POST /v1/speech
| Field | Type | Notes |
|---|---|---|
text | string | Required. Up to 100,000 characters. Markdown is stripped automatically. |
voice | string | Optional, defaults to af_heart. See voices below. |
speed | number | Optional, 0.5–2.0. Defaults to 1. |
Text under 1,200 characters renders inside the request and comes back with status: done. Anything longer returns 202 with a job to poll — audio parts appear progressively so you can begin playback early.
GET /v1/jobs/{id}
Poll a render. audio_urls grows as parts complete; status becomes done when the last one lands.
// Long text (a chapter, an article) returns 202 with a job id.
let job = await start(text);
while (job.status === "queued" || job.status === "processing") {
await new Promise((r) => setTimeout(r, 3000));
const res = await fetch(`https://audio.chanovalabs.com/v1/jobs/${job.id}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
job = await res.json();
// audio_urls grows as parts finish — you can start playing part 0
// while the rest is still rendering.
}GET /v1/voices
Lists the narrators. No authentication needed.
| Voice id | Name | Character |
|---|---|---|
af_heart | Heart | Warm, natural female (US) — the house voice |
af_bella | Bella | Expressive female (US) |
af_nicole | Nicole | Soft, close-mic female (US) |
bf_emma | Emma | Female (UK) |
am_fenrir | Fenrir | Male (US) |
In your language
JavaScript / TypeScript
const res = await fetch("https://audio.chanovalabs.com/v1/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CHANOVA_AUDIO_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, voice: "af_heart" }),
});
const job = await res.json();
// Short text is ready immediately; long text renders in the background.
if (job.status !== "done") {
// poll until the parts you need are available
}
audio.src = job.audio_urls[0];Python
import os, requests
r = requests.post(
"https://audio.chanovalabs.com/v1/speech",
headers={"Authorization": f"Bearer {os.environ['CHANOVA_AUDIO_KEY']}"},
json={"text": text, "voice": "af_heart"},
timeout=120,
)
job = r.json()
print(job["audio_urls"])Credits & caching
One credit per character of text. New accounts start with 100,000 credits. Identical text in the same voice is served from cache and costs nothing, so re-requesting a page your users read often is free. If a render fails, the credits are returned automatically.
Speech to text
The same key transcribes audio back into text. POST /v1/transcribe accepts a file upload, a url, or audio_base64 — anything ffmpeg can read, including video. Audio under 30 seconds returns inline; longer recordings return 202 with a job to poll at /v1/transcripts/{id}.
Request
curl https://audio.chanovalabs.com/v1/transcribe \
-H "Authorization: Bearer $CHANOVA_AUDIO_KEY" \
-F "file=@interview.mp3" \
-F "language=en"Response
{
"tok-key">"id": "6a7ce1eb0faa8f4b64f49c48",
"tok-key">"status": "done",
"tok-key">"model": "onnx-community/whisper-small.en_timestamped",
"tok-key">"language": "en",
"tok-key">"duration_seconds": 45,
"tok-key">"cached": false,
"tok-key">"text": "Every book you love, the ones you've read twice...",
"tok-key">"segments": [
{ "tok-key">"text": "Every", "tok-key">"start": 0, "tok-key">"end": 0.26 },
{ "tok-key">"text": "book", "tok-key">"start": 0.26, "tok-key">"end": 0.46 }
],
"tok-key">"credits_remaining": 199240
}JavaScript
const form = new FormData();
form.append("file", fileInput.files[0]);
form.append("language", "en");
const res = await fetch("https://audio.chanovalabs.com/v1/transcribe", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.CHANOVA_AUDIO_KEY}` },
body: form,
});
const { text, segments } = await res.json();
// segments carry word-level start/end — enough to build captionsPolling a long recording
// Audio over 30s returns 202 with a job id.
let job = await (await fetch(url, { method: "POST", body: form })).json();
while (job.status !== "done" && job.status !== "error") {
await new Promise((r) => setTimeout(r, 2000));
job = await (await fetch(`https://audio.chanovalabs.com/v1/transcripts/${job.id}`, {
headers: { Authorization: `Bearer ${KEY}` },
})).json();
}Every response carries word-level start and end times, which is what you need to build captions or subtitles. Pass word_timestamps=false for sentence-level segments instead — it is faster and the payload is much smaller.
| model | Speed | Use it when |
|---|---|---|
small.en | ~5x realtime | Default. Best accuracy on English, including names. |
base.en | ~11x realtime | You want speed and the audio is clean. |
multilingual | ~5x realtime | The audio is not English. Selected automatically when you pass a non-English language. |
Transcription is billed from the same balance as speech, at 14 credits per second of audio — the same rate per second that synthesis costs. An hour of audio is about 50,000 credits. As with speech, an identical file is only ever transcribed once: repeats are served from cache and cost nothing, and a failed run is refunded automatically.
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_api_key | Missing or revoked key. |
| 402 | insufficient_credits | Balance too low for this request. |
| 400 | text_too_long | Over 100,000 characters — split it up. |
| 500 | synthesis_failed | Render failed; credits were refunded. |
| 400 | decode_failed | That file could not be decoded as audio. |
| 400 | audio_too_long | Recording over 2 hours — split it up. |
| 413 | audio_too_large | Upload over 100 MB — use the `url` form. |
Ready to build?
Get your API key