Voixa API
One REST API for voices, speech, transcription and your audio library, in Vietnamese, English, Chinese, Japanese, Korean, French, German, Italian, Spanish and Portuguese. Base URL https://api.voixa.vovix.io/v1. Authenticate with an API key from Studio in the x-api-key header.
Speak
Send text and a voice id; get a clip back. Pass an optional project (e.g. episode-12) to group clips by content and list them with GET /clips?project=. Every clip also records where it came from: source is api for calls with an API key and studio for clips made in Studio. The API waits up to ~20 seconds for the audio. If the engine was cold you receive status: "processing" and poll GET /clips/{clipId} until it is ready.
curl -X POST https://api.voixa.vovix.io/v1/speak \
-H "x-api-key: $VOIXA_API_KEY" \
-H "content-type: application/json" \
-d '{"voiceId":"vi-truc-ly","text":"Xin chào, đây là Voixa.","project":"episode-12"}'
# Query strings must be quoted in most shells (zsh treats ? as a glob)
curl "https://api.voixa.vovix.io/v1/clips?project=episode-12&source=api" -H "x-api-key: $VOIXA_API_KEY"Podcasts
Up to 100 000 characters in one call. Voixa splits the script into sentences, produces the parts in parallel and joins them into a single WAV; blank lines become short pauses. The clip comes back as status: "processing" with kind: "podcast", parts and partsDone; poll GET /clips/{clipId}. An 80-minute episode takes roughly 20 minutes in Vietnamese and a few minutes in the other languages.
curl -X POST https://api.voixa.vovix.io/v1/podcasts \
-H "x-api-key: $VOIXA_API_KEY" -H "content-type: application/json" \
-d '{"voiceId":"vi-truc-ly","title":"Episode 12","text":"<up to 100 000 characters>","project":"my-show"}'Transcribe
The other direction: an audio or video file becomes text with timed segments, SRT and VTT. Up to 5 GB and eight hours per file; WAV, MP3, M4A, MP4, OGG, WebM, FLAC and AAC (video files are fine — only the audio track is read). One API call, then the bytes: the API hands you a signed upload URL, and the job starts by itself the moment the upload lands. The bytes never pass through the API, which is what lets files be this large.
Transcription runs on a machine that starts on demand, so it is never synchronous: the transcript comes back as status: "processing" and you poll GET /transcripts/{id}. Expect two to three minutes before recognition starts on a cold queue, then about one minute of work per five minutes of audio. While it runs, doneSec, durationSec and partsDone report progress.
# 1. one call: register the transcript and get a place to upload to
curl -X POST https://api.voixa.vovix.io/v1/transcribe \
-H "x-api-key: $VOIXA_API_KEY" -H "content-type: application/json" \
-d '{"format":"mp3","fileName":"interview.mp3","language":"vi","project":"episode-12"}'
# → { "transcript": { "transcriptId": "…", "status": "processing" }, "uploadUrl": "https://…" }
# 2. the bytes, straight to storage — this starts the job
curl -X PUT "$UPLOAD_URL" -H "content-type: audio/mpeg" --data-binary @interview.mp3
# 3. poll until it is ready
curl https://api.voixa.vovix.io/v1/transcripts/… -H "x-api-key: $VOIXA_API_KEY"SDK
npm install @vovix/voixa — zero dependencies, works in Node 18+, Bun, Deno and edge runtimes.
import { Voixa } from '@vovix/voixa'
const vx = new Voixa({ apiKey: process.env.VOIXA_API_KEY })
// Built-in voices and your clones
const { voices } = await vx.voices({ language: 'vi' })
// Text → clip. say() waits until the audio is ready; speak() returns at once.
const clip = await vx.say({ voiceId: voices[0].voiceId, text: 'Xin chào.', project: 'episode-12' })
clip.url // signed WAV URL, valid for 1 hour
clip.source // 'api' — clips made in Studio carry 'studio'
// Your library, grouped by project
const { projects } = await vx.projects() // [{ project, clips, chars }]
const { items } = await vx.listClips({ project: 'episode-12', source: 'api' })
// Public link for one clip (no login needed to listen)
const { shareUrl } = await vx.shareClip(clip.clipId)
// Clone a voice from 3–10 s of clean speech; refText is what the sample says (optional, sharpens Chinese/Japanese/Korean)
const mine = await vx.createVoice({ name: 'Me', language: 'en', audio: wavBytes, refText: 'Hi, this is my voice.' })
await vx.say({ voiceId: mine.voiceId, text: 'Now in my own voice.', project: 'episode-12' })// Speech to text: audio (or video) in, transcript with timestamps out.
const t = await vx.transcribeFile({
audio: await fs.readFile('interview.mp3'),
fileName: 'interview.mp3',
// language: 'vi', // optional — Voixa detects it on its own
// task: 'translate', // recognise any language, return English
// words: true, // per-word timestamps
})
t.language, t.durationSec // 'vi', 1842
t.text // the whole transcript
t.segments[0] // { id: 0, start: 0.0, end: 4.2, text: '…' }
t.srtUrl, t.vttUrl, t.txtUrl // signed subtitle/text files, valid for 1 hour
// Without waiting (the job runs on its own machine):
const { transcript } = await vx.transcribe({ audio, fileName: 'talk.m4a', project: 'episode-12' })
const done = await vx.waitForTranscript(transcript.transcriptId) // doneSec / durationSec while it runsvoices({ language? }), getVoice(id) | Built-in voices plus your clones; mine and shared flags |
samples({ language? }) | Built-in voices with a public sample URL, for a voice picker in your own app |
speak({ voiceId, text, project?, wait? }) | Returns { clip }; wait: false returns immediately with status: "processing" |
say(req, { timeoutMs?, pollMs? }) | speak plus polling until the clip is ready (throws if it fails) |
createPodcast({ voiceId, title?, text, project? }), makePodcast(req) | Long text as one episode; makePodcast waits for it (up to two hours) |
listClips({ project?, source?, kind?, voiceId?, limit?, cursor? }) | Your library, newest first; cursor for the next page |
projects() | Your content groups with clip and character counts |
getClip(id), waitFor(id), deleteClips([ids]) | One clip with a fresh signed URL; poll until ready; delete |
shareClip(id), unshareClip(id) | Public listening page (shareUrl) for one clip, and revoke it |
createVoice({ name, language, audio, format?, gender?, description?, refText? }) | Upload a reference recording and wait for the clone and its sample |
updateVoice(id, { name?, description?, gender?, styles?, shared? }), deleteVoice(id) | Manage your clones; shared: true lets every Voixa account use the voice |
transcribe({ audio, fileName?, title?, language?, task?, words?, prompt?, project? }) | Registers and uploads in one go; returns { transcript } with status: "processing" |
transcribeFile(req, { timeoutMs?, pollMs? }) | transcribe plus polling until the transcript is ready (throws if it fails) |
getTranscript(id), waitForTranscript(id) | One transcript with text, segments and signed JSON/SRT/VTT/TXT URLs |
listTranscripts({ project?, source?, status?, limit?, cursor? }), deleteTranscripts([ids]) | Your transcripts, newest first (no text in lists); delete removes the upload too |
sttLanguages() | Languages you may pin for transcription, accepted formats and limits |
languages(), account() | Supported languages with limits and clone guide; your allowances and usage |
Errors throw VoixaError with status and code (see below). Types: Clip carries clipId, voiceId, text, chars, project?, source, kind?, title?, parts?, partsDone?, durationSec?, status, url?, shareUrl?; Voice carries voiceId, kind, language, name, gender, styles, sampleUrl?, mine, shared, status.
Endpoints
GET /languages | Supported languages with labels, character limit, sample sentence and clone-recording guide |
GET /samples?language=vi|en|zh|ja|ko|fr|de|it|es|pt | Built-in voices with a public sample URL and the sentence spoken — for a voice picker in your own app |
GET /voices?language=…, GET /voices/{voiceId} | Built-in voices plus your clones (and clones other users shared) |
POST /speak | { voiceId, text, project?, wait? } → { clip } |
POST /podcasts | { voiceId, title?, text, project? } → { clip } with kind: "podcast", always processing at first |
GET /clips?project=&source=api|studio&kind=clip|podcast&voiceId=&limit=&cursor= | Your library, newest first; filter by project, origin or voice. Every clip carries source and its project |
GET /projects | Your content groups with clip and character counts |
GET /clips/{clipId} | One clip, with a signed URL when ready |
POST /clips/{clipId}/share, DELETE /clips/{clipId}/share | Create or revoke a public listening link (shareUrl) |
DELETE /clips | { clipIds: [] } |
POST /voices/upload | { format: "wav"|"mp3" } → upload URL for the reference recording |
POST /voices | { voiceId, name, language, gender?, description?, refText? } → your cloned voice with a sample |
PATCH /voices/{voiceId}, DELETE /voices/{voiceId} | Manage your clones (name, description, gender, styles, shared) |
POST /transcribe | { format, title?, fileName?, language?, task?, words?, prompt?, project? } → { transcript, uploadUrl, expiresIn, maxBytes }. PUT the bytes to uploadUrl within 15 minutes; the job starts on arrival |
GET /transcripts?project=&source=&status=&limit=&cursor= | Your transcripts, newest first (summaries: no text or segments) |
GET /transcripts/{transcriptId} | One transcript with text, segments and signed JSON/SRT/VTT/TXT/audio URLs |
DELETE /transcripts | { transcriptIds: [] } — also deletes the uploaded audio and cancels a running job |
GET /stt/languages | Languages for transcription, accepted formats and limits |
GET /account | Daily allowances and usage (characters for speech, seconds of audio for transcription) |
GET /keys, POST /keys, DELETE /keys/{keyId} | API keys (Studio login only) |
Limits and errors
- Up to 5 000 characters per
speakcall (1 200 for Chinese, Japanese and Korean) and 100 000 per podcast; the daily character allowance is per account (GET /account). Line breaks and runs of spaces are collapsed to a single space before counting, so pasted text costs no more than it reads. - API keys: 1 request per second, 1 000 calls per day. A new key becomes active within a few minutes (until then the gateway answers
Forbidden). Signed URLs last one hour; share links do not expire until revoked. - Transcription: up to 5 GB and eight hours per file (5 GB is what a single upload can carry; longer recordings should be split); the daily allowance is counted in seconds of audio, separately from characters (
dailyAudioSecandaudioUsedTodayinGET /account). Recognition understands far more languages than Voixa speaks — seeGET /stt/languages. project: letters, digits, spaces and. _ -, at most 60 characters. Clips and transcripts share the same project names.- Voice cloning is available for Vietnamese, English, French, German, Italian, Spanish and Portuguese. Chinese, Japanese and Korean use built-in voices only (
GET /languagesreportscloneable). - Errors are JSON with
errorand acode:QUOTA(429, withused,limit,resetsAt),CLONE_LIMIT,VOICE_NOT_READY,CLIP_NOT_READY,NO_REFERENCE,REFERENCE_EMPTY,REFERENCE_TOO_LARGE,AUDIO_QUOTA(429),AUDIO_TOO_LARGE,AUDIO_EMPTY,NO_AUDIO,INVALID(400),NOT_APPROVED(403). - New accounts can generate audio right away within the default daily allowance; contact us to raise it.