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

FieldTypeNotes
textstringRequired. Up to 100,000 characters. Markdown is stripped automatically.
voicestringOptional, defaults to af_heart. See voices below.
speednumberOptional, 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 idNameCharacter
af_heartHeartWarm, natural female (US) — the house voice
af_bellaBellaExpressive female (US)
af_nicoleNicoleSoft, close-mic female (US)
bf_emmaEmmaFemale (UK)
am_fenrirFenrirMale (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 captions

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

modelSpeedUse it when
small.en~5x realtimeDefault. Best accuracy on English, including names.
base.en~11x realtimeYou want speed and the audio is clean.
multilingual~5x realtimeThe 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

StatusCodeMeaning
401invalid_api_keyMissing or revoked key.
402insufficient_creditsBalance too low for this request.
400text_too_longOver 100,000 characters — split it up.
500synthesis_failedRender failed; credits were refunded.
400decode_failedThat file could not be decoded as audio.
400audio_too_longRecording over 2 hours — split it up.
413audio_too_largeUpload over 100 MB — use the `url` form.

Ready to build?

Get your API key