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 runs
voices({ 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 /languagesSupported languages with labels, character limit, sample sentence and clone-recording guide
GET /samples?language=vi|en|zh|ja|ko|fr|de|it|es|ptBuilt-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 /projectsYour 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}/shareCreate 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/languagesLanguages for transcription, accepted formats and limits
GET /accountDaily 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