Public API v1

Submit a TTS generation job

Changed on

Submits a text-to-speech job. Returns immediately with a jobId. Poll GET /v1/tts/{jobId} — or GET /v1/tts?ids=… for several jobs at once — to check progress and retrieve the audio URL when complete. When identical audio already exists (same text, voice, speed, emotion and engine), the response is status: "completed" with audioUrl included — nothing to poll. Tokens are deducted on submission, billed by the submitted character count (minimum 50, × the AI surcharge only when you opt in with aiRefine: true), and refunded if the job permanently fails. Send an Idempotency-Key header whenever you might retry (timeouts, connection resets, a 502 from the edge): the same key with the same body returns the FIRST job instead of creating and charging a second one.

post/api/v1/tts

Request

  • Base URL
    https://api.vieneu.io
  • Auth
    • API key in header X-API-Key
    • or HTTP bearer

Headers

Idempotency-Keystring

Optional. A client-chosen key (8–128 printable ASCII characters — a UUID is ideal) that makes this call safe to retry. A second POST with the same key AND the same body returns the FIRST job (same jobId, no second charge) with the response header Idempotent-Replayed: true. Keys are scoped to your API key and remembered for 24 hours. Same key + different body → 422 IDEMPOTENCY_KEY_REUSED; same key while the first call is still running → 409 IDEMPOTENCY_IN_FLIGHT with Retry-After — retry with the SAME key, never a new one.

Request body

textstring required

Vietnamese text to synthesize.

voiceIdstring

Voice ID — must be one returned by GET /v1/voices for the requested engine. The display name (name) is accepted too: "Thiện Minh" renders voice "Anh Khôi". Omit to use that engine's default voice.

emotion'natural' | 'storytelling' | 'tin_tuc' | 'tu_nhien' | 'doc_truyen' | 'emotion_0' | 'emotion_1' | 'emotion_2' | 'emotion_3' | 'emotion_4' | 'emotion_5' | 'emotion_6' | 'emotion_7' | 'emotion_8' | 'emotion_9' | 'emotion_10' | 'emotion_11' | 'emotion_12' | 'emotion_13' | 'emotion_14' | 'emotion_15' | 'emotion_16' | 'emotion_17' | 'emotion_18' | 'emotion_19'
speednumber

Playback rate of the synthesized speech. 1.0 is the natural pace.

engine'v4'

TTS engine. Only "v4" (48 kHz, reference-based voices) is served: "v3" was retired on 2026-09-24 and a request naming it is refused with 400. Omit it to use the configured default — GET /v1/engines reports the live answer as isDefault, next to each engine’s billing multiplier. The voice must belong to the engine you pick (GET /v1/voices?engine=…); V3 voice ids do not exist on V4.

aiRefineboolean

AI text refinement. Defaults to FALSE on the public API: the text is synthesized as submitted, with no AI moderation and no AI normalization, billed at the plain per-character rate. Set true to run the same AI step the web app uses — formulas, acronyms and mixed-in English are read correctly and the content is checked — billed with the AI surcharge and one extra model round-trip of latency. Deterministic text preparation is applied either way, and so is your pronunciation dictionary (see applyPronunciation).

applyPronunciationboolean

Read the text through your account's pronunciation dictionary — the rules saved at https://vieneu.io/#/pronunciation (Từ điển phát âm). Defaults to TRUE, and changes nothing until you have saved rules there. The rules run after aiRefine and before synthesis; you are billed for the text you sent, and that is the text kept with the job. Set false to read this request as written. If your rules make the text longer than the request limit, or more than twice as long plus 1,000 characters, the call is refused with 400 PRONUNCIATION_EXPANDED_TOO_LONG, uncharged.

Example request

{
  "text": "Xin chào Việt Nam!",
  "voiceId": "Trúc Ly",
  "emotion": "natural",
  "speed": 1,
  "engine": "v4",
  "applyPronunciation": true
}

Response

Job accepted: queued (poll GET /api/v1/tts/{jobId}) or, when the audio was already in the cache, completed with audioUrl. When the call replayed an earlier submit under the same Idempotency-Key, the body names the original job as queued and the response carries Idempotent-Replayed: true.

jobIdstring required
status'queued' | 'completed' required

queued — poll GET /v1/tts/{jobId} for the result. completed — the audio already existed (same text, voice, speed, emotion and engine as an earlier request, by anyone) and audioUrl is in this response: nothing to poll.

messagestring required
audioUrlstring

Presigned download URL, only when status is completed. Valid for 24 hours — the same URL the poll would return.

audioUrlExpiresInnumber

Seconds until audioUrl expires. Only with status: completed.

durationnumber

Audio length in seconds. Only with status: completed.

Example response

{
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "queued",
  "message": "Job queued. Poll GET /v1/tts/{jobId} for status and audio URL.",
  "audioUrl": "https://s3.amazonaws.com/bucket/tts/job.wav?X-Amz-Expires=86400&...",
  "audioUrlExpiresIn": 86400,
  "duration": 3.42
}

Changes