---
title: "OpenAI-compatible speech synthesis"
method: POST
path: "/api/v1/audio/speech"
tags: ["Public API v1"]
---

# OpenAI-compatible speech synthesis

`POST /api/v1/audio/speech`

Drop-in for OpenAI `POST /v1/audio/speech`: point an OpenAI SDK at `https://api.vieneu.io/api/v1` with your VieNeu key and it works unchanged. Returns the audio bytes directly — mp3 by default, like OpenAI; `wav`, `opus` and `pcm` are also available via `response_format`. Set `stream_format` to `audio` or `sse` to receive audio as it is generated instead of waiting for the whole file — that requires `response_format: pcm` or `ulaw`, the only two encodings whose frames concatenate. Maps `input`→text, `voice`→VieNeu voice id (from GET /v1/audio/voices) and `model`→engine. Billed by the submitted character count (minimum 50, × the AI surcharge only if you opt in with `aiRefine: true`) and refunded if synthesis fails. Auth via `Authorization: Bearer <api key>`.

## Request body

- OpenAiSpeechRequestDto
  - `model` string — Selects the engine. OpenAI model names (`tts-1`, `tts-1-hd`, `gpt-4o-mini-tts`) map to the default engine so an unmodified OpenAI client works; `vieneu-v3` and `vieneu-v4` select an engine explicitly. Any other value is accepted and ignored, as this endpoint has always done. A `vieneu-…` name for an engine that does not exist IS rejected — that is a choice of engine, and rendering on a different one would bill at a rate you did not pick. `engine` wins if both are sent.
  - `input` string, required — The text to synthesize (OpenAI: `input`). Required.
  - `voice` string — VieNeu voice id from GET /v1/voices (e.g. "Ngọc Lan"). Omit to use the default voice. (OpenAI voice names like "alloy" are not mapped — pass a VieNeu voice id.)
  - `response_format` 'wav' | 'mp3' | 'opus' | 'pcm' | 'ulaw' — Output format. Defaults to `mp3`, as OpenAI does — a client that omits the field expects mp3, and returning WAV to it produced a file its player refused. `wav`, `opus`, `pcm` and `ulaw` are also supported; `pcm` is raw headerless signed 16-bit little-endian at 24 kHz — the rate OpenAI documents, and what this route defaults to unless `sample_rate` says otherwise — and `ulaw` is raw 8 kHz G.711 for telephony. OpenAI's `aac` and `flac` are not supported and return 400.
  - `sample_rate` 8000 | 16000 | 22050 | 24000 | 44100 | 48000 — VieNeu extension: output sample rate in Hz. Omit for 48000 — except with `pcm`, which defaults to 24000 (the rate OpenAI documents, since an OpenAI client has no field to ask for another and headerless PCM at the wrong rate plays at the wrong speed), and `ulaw`, which is always 8000.
  - `speed` number — Playback rate, applied by a pitch-preserving time-stretch. OpenAI accepts 0.25–4.0 and so does this endpoint, but VieNeu's engine only holds quality across 0.5–2.0, so values outside that are CLAMPED, not rejected — a 400 for a value OpenAI itself considers valid would break the drop-in promise. The native /v1 endpoints validate 0.5–2.0 strictly instead.
  - `instructions` string — Accepted and ignored. It exists because `gpt-4o-mini-tts` — one of the model names this endpoint accepts — is the model whose whole point is this parameter, and the global validation pipe rejects unknown fields. Sending it must not 400; VieNeu conveys style through `emotion` and inline cues.
  - `stream_format` 'audio' | 'sse' — Stream the response instead of buffering the whole file. `audio` sends the encoded bytes as they are produced (chunked transfer). `sse` sends OpenAI-shaped Server-Sent Events: `speech.audio.delta` carrying base64 audio, then a final `speech.audio.done`. That final event is the only positive proof the stream finished — without it, treat the audio as truncated. ONLY valid with `response_format: pcm` or `ulaw`: streamed frames are encoded independently, so mp3/opus/wav cannot be concatenated into one playable file and are rejected with 400. Note `response_format` defaults to mp3, so streaming requires setting it explicitly.
  - `emotion` string — VieNeu extension: speaking style — 'natural' or 'storytelling'.
  - `aiRefine` boolean — VieNeu extension — AI text refinement. Defaults to FALSE on the public API: the text is synthesized as submitted, with no AI moderation and no pronunciation 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.
  - `engine` 'v3' | 'v4' — VieNeu extension: TTS engine — "v3" (48 kHz, default) or "v4" (48 kHz, premium). Omit for the configured default.

## Response `200`

The audio bytes. Content-Type follows `response_format` (mp3 by default). With `stream_format: "sse"` the body is instead a `text/event-stream` of base64 audio deltas; with `"audio"` it is the same bytes, chunked. For the headerless `pcm` and `ulaw` formats the sample rate is in `X-Sample-Rate` (`pcm` defaults to 24 kHz here, not the engine native rate).

## Other responses

- `400` — OpenAI-shaped error: missing `input`, unknown voice, unsupported format.
- `401` — OpenAI-shaped error: API key missing, malformed, or revoked.
- `403` — OpenAI-shaped error: the grant is out of tokens, has expired, or the plan does not allow the requested engine. A daily/weekly cap comes back as 429 instead.
- `429` — OpenAI-shaped error: rate limit or token quota. See `Retry-After`.
- `503` — OpenAI-shaped error: no worker available for the requested engine or format. Retry shortly.

---

[API](https://skmtc.dev/vieneu/apis/vieneu-api.md) · [All operations](https://skmtc.dev/vieneu/apis/vieneu-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/vieneu/vieneu-api/revisions/11c8166a6c4d/schema)
