# Tổng quan Cloud API

Source: https://docs.vieneu.io/vi/docs/cloud-api/overview

VieNeu Cloud API biến văn bản tiếng Việt thành giọng nói qua HTTPS. Bạn gửi text
và một voice id, bạn nhận lại audio — không phải tải model, không phải thuê GPU.

:::note Hai sản phẩm khác nhau
Phần này nói về **API đám mây** tại `api.vieneu.io`. Phần **SDK** nói về gói chạy
on-device tách biệt, chạy model ngay trên máy bạn. Hai bên dùng chung bộ giọng và
cái tên, ngoài ra không chung gì nữa: cài khác nhau, tính phí khác nhau, code
khác nhau. Nếu bạn đang tích hợp VieNeu vào một sản phẩm, gần như chắc chắn bạn
cần phần này.
:::

Nếu bạn muốn nghe được audio trước khi đọc bất cứ thứ gì ở đây,
**[Bắt đầu nhanh](./quickstart.md)** chỉ gồm một key, một giọng và một lời gọi.

## Tài liệu tham chiếu đầy đủ {#the-full-reference}

Mọi endpoint, mọi field, mọi lỗi đều nằm trong **[API reference](/api-reference)**,
dựng từ đặc tả OpenAPI do chính server sinh ra. Không cần đăng nhập.

Chính tài liệu đó cũng được phục vụ dưới dạng file thuần tại
**[`/openapi.json`](pathname:///openapi.json)** — trỏ `openapi-generator`,
`oapi-codegen`, Kiota hay công cụ tương đương trong ngôn ngữ của bạn vào đó là
bạn có ngay một client đã sinh sẵn kiểu dữ liệu:

<!-- `pathname://` keeps Docusaurus from treating this as a build asset. Without
     it the markdown loader copies the file to /assets/files/openapi-<hash>.json
     and links THAT — a URL that changes on every spec change, and one that does
     not match the stable path the curl command below tells people to use. -->


```bash
curl -O https://docs.vieneu.io/openapi.json
```

File này sinh ra từ chính định nghĩa route của API, và CI sẽ đánh trượt build nếu
bản đã commit không còn khớp với chúng — nên đặc tả không thể âm thầm tụt lại sau
phần *khai báo* trong code.

Đó là một bảo đảm hẹp hơn ta tưởng, và nên nói thẳng ra: phép kiểm so bản JSON
đã commit với các decorator, chứ không so với thứ server thực sự làm. Một
decorator mô tả sai một endpoint sẽ cho ra một đặc tả sai kèm một phép kiểm màu
xanh — và đó đúng là cách endpoint nhân bản giọng vẫn tiếp tục khai báo quyền
chọn engine suốt nhiều tháng sau khi server đã bắt đầu từ chối nó. Chỗ nào tài
liệu này và đặc tả nói khác nhau về hành vi, các trang ở đây mới là bản được viết
dựa trên code đang chạy.

Đặc tả chỉ bao phủ `/api/v1` — các endpoint dashboard, thanh toán và quản trị của
chính ứng dụng không nằm trong hợp đồng công khai.

## Base URL {#base-url}

```
https://api.vieneu.io/api/v1
```

## Xác thực {#authentication}

Mọi request đều mang API key của bạn, đặt kiểu nào cũng được:

```bash
-H "Authorization: Bearer vn_sk_..."     # or
-H "X-API-Key: vn_sk_..."
```

Key bắt đầu bằng `vn_sk_` là key thật; key `vn_test_` hoạt động y hệt nhưng chặn
mỗi request ở 100 từ. Tạo loại nào cũng được tại
**[trang Nhà phát triển](https://www.vieneu.io/#/developer?create=test)** trong web app
VieNeu — chuỗi key gốc chỉ hiện đúng một lần, lúc tạo, và không bao giờ hiện lại.

Key luôn được cấp dựa trên một **token grant đang hoạt động**, nên tài khoản mới
phải mở một grant trước thì trang mới chịu cấp key; không có grant, thao tác tạo
trả về `403` kèm `No active API token grant found.` Trang Nhà phát triển có bản
dùng thử 7 ngày miễn phí để mở grant đó, mỗi tài khoản một lần.

## Hai cách tổng hợp giọng nói {#the-two-ways-to-synthesize}

**Đồng bộ** — bạn nhận thẳng các byte audio trong response:

```bash
curl https://api.vieneu.io/api/v1/audio/speech \
  -H "Authorization: Bearer $VIENEU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": "Xin chào, đây là VieNeu.", "voice": "Ngọc Lan" }' \
  --output speech.mp3
```

Đây là [endpoint tương thích OpenAI](./openai-compatible.md) — đường vào nhanh
nhất nếu bạn đã có sẵn client OpenAI, và là lựa chọn dành cho text ngắn.

**Bất đồng bộ** — với text dài, gửi một job rồi poll nó:

```bash
curl -X POST https://api.vieneu.io/api/v1/tts \
  -H "Authorization: Bearer $VIENEU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "…", "voiceId": "Ngọc Lan" }'
# → { "jobId": "…", "status": "queued" }
#   hoặc, khi đúng đoạn audio đó đã có sẵn (trúng cache):
#   { "jobId": "…", "status": "completed", "audioUrl": "https://…" } — không cần poll

curl https://api.vieneu.io/api/v1/tts/<jobId> \
  -H "Authorization: Bearer $VIENEU_API_KEY"
# → { "status": "completed", "audioUrl": "https://…" }
```

Còn khi bạn cần audio bắt đầu phát trước lúc toàn bộ text sinh xong, hãy dùng
[streaming](./streaming.md).

## Toàn bộ operation {#every-operation}

Ba mươi hai operation trên hai mươi bảy path — trọn bề mặt công khai. Mỗi cái làm gì,
và không nói gì về field của nó: request body chỉ nằm ở
[API reference](/api-reference) mà thôi, vì thêm một bản mô tả API là thêm một thứ
phải giữ cho đúng, và cái cũ đi bao giờ cũng không phải bản sinh tự động.

Trang reference cho phép deep-link theo operation id, nên `operationId` bên dưới
cũng chính là cách bạn nhảy thẳng tới một mục — `/api-reference#operation/createTtsJob`
— và là cách bạn gọi tên nó cho một client sinh tự động.

**Tổng hợp giọng nói**

| Operation | `operationId` | Chức năng |
|---|---|---|
| `POST /v1/audio/speech` | `createSpeech` | Tổng hợp tương thích OpenAI, đồng bộ hoặc streaming |
| `POST /v1/tts` | `createTtsJob` | Gửi một job bất đồng bộ |
| `GET /v1/tts/{jobId}` | `getTtsJob` | Poll job để lấy trạng thái và URL tải về |
| `POST /v1/tts/stream` | `streamSpeech` | Streaming theo khung, độ trễ thấp |
| `POST /v1/dialogue` | `createDialogue` | Hội thoại nhiều người nói trong một lời gọi |
| `POST /v1/dub` | `createDub` | Lồng lại giọng cho một bản ghi có sẵn |
| `POST /v1/srt` | `createSrtDub` | Lồng tiếng một file phụ đề thành track khớp timecode |
| `POST /v1/vapi/speech` | `vapiSpeech` | Webhook giọng tuỳ chỉnh của [Vapi](./vapi.md) — PCM thô cho voice agent |

**Giọng và metadata**

| Operation | `operationId` | Chức năng |
|---|---|---|
| `GET /v1/voices` | `listVoices` | Danh mục giọng, kèm giọng nhân bản của riêng bạn khi có xác thực. Không cần key |
| `GET /v1/audio/voices` | `listOpenAiVoices` | Chỉ id, dành cho client tương thích OpenAI. Cái này **có** cần key |
| `GET /v1/engines` | `listEngines` | Các engine đang chạy, tính năng và hệ số tính phí. Không cần key |
| `GET /v1/emotion-tags` | `listEmotionTags` | Kiểu đọc và thẻ chỉ dẫn đặt ngay trong text, theo từng engine. Không cần key |

**Số dư và mức dùng**

| Operation | `operationId` | Chức năng |
|---|---|---|
| `GET /v1/balance` | `getBalance` | Số token key này tiêu được ngay lúc này (mỗi lời gọi do một gói trả: gói của key, hoặc một gói khác còn hoạt động khi gói đó không trả được), gói đó và hạn mức ngày / tuần. Không trừ token |
| `GET /v1/usage` | `getUsage` | Key này (hoặc cả tài khoản) đã làm gì trong một khoảng thời gian: số lời gọi, token, giây audio, theo ngày / route / giọng, lỗi |

**Nhân bản giọng — chỉ làm trên web**

Giọng nhân bản được tạo trên Studio web tại
[vieneu.io/#/clone](https://vieneu.io/#/clone), không qua API này. Tạo giọng là
việc có người dẫn: Studio khử nhiễu clip, tự chép lời, và cho bạn NGHE THỬ trước
khi lưu. Một clip mẫu tồi làm hỏng MỌI lần tạo tiếng sau đó với giọng ấy chứ
không chỉ một request, nên API không còn nhận đường tắt này nữa.
`POST /v1/voices`, `DELETE /v1/voices/{voiceId}`, `POST /v1/clone`,
`POST /v1/upload` và `POST /v1/prepare` trả `410` kèm `code: "CLONE_WEB_ONLY"`
và không tính một token nào.

Còn DÙNG giọng nhân bản qua API thì không đổi: giọng hiện trong `GET /v1/voices`
khi bạn gửi kèm key (`"kind": "cloned"`), và id `clone_…` của nó là một `voiceId`
bình thường trên `POST /v1/tts` và `POST /v1/tts/stream`, giá đúng bằng giọng
trong catalogue. Xem [Giọng nhân bản](./streaming.md#cloned-voices).

**Webhooks** — xem [Webhooks](./webhooks.md) để biết hợp đồng sự kiện và một đoạn
kiểm chữ ký bạn có thể dán thẳng vào.

| Operation | `operationId` | Chức năng |
|---|---|---|
| `POST /v1/webhooks` | `createWebhookEndpoint` | Đăng ký một endpoint. Trả về signing secret đúng một lần |
| `GET /v1/webhooks` | `listWebhookEndpoints` | Liệt kê các endpoint của bạn |
| `GET /v1/webhooks/{endpointId}` | `getWebhookEndpoint` | Đọc một endpoint |
| `DELETE /v1/webhooks/{endpointId}` | `deleteWebhookEndpoint` | Xoá một endpoint |
| `GET /v1/webhooks/{endpointId}/deliveries` | `listWebhookDeliveries` | Các lần gửi gần đây, kèm lý do thất bại |
| `POST /v1/webhooks/{endpointId}/rotate-secret` | `rotateWebhookSecret` | Xoay signing secret, cái cũ còn hiệu lực 24 giờ |

**Sách nói** — một giọng dẫn chuyện và tối đa 30 giọng nhân vật đọc các chương
viết dưới dạng kịch bản. Sách được tạo ở chế độ nền, mỗi lần một chương, vào lúc
cụm GPU rảnh: tính bằng giờ chứ không phải giây. Các cuốn cùng chờ được tạo xen
kẽ, mỗi lượt một chương. Mỗi chương bị trừ token theo ký
tự khi bắt đầu tạo, được hoàn nếu lỗi, được xử lý âm thanh thành MP3 và lưu vào
Thư viện. Các chương không phát sự kiện webhook `tts.job.*` — hãy theo dõi cả
cuốn. Cùng tính năng ngay trong khung chat: [Sách nói](../integrations/mcp/audiobooks.md).

| Operation | `operationId` | Chức năng |
|---|---|---|
| `POST /v1/audiobooks` | `createAudiobook` | Tạo bản nháp: tên sách, giọng dẫn chuyện, các vai. Chỉ key LIVE |
| `GET /v1/audiobooks` | `listAudiobooks` | Các sách của bạn, mới nhất trước |
| `GET /v1/audiobooks/{bookId}` | `getAudiobook` | Tiến độ, và link tải (24 giờ) của từng chương đã xong |
| `POST /v1/audiobooks/{bookId}/chapters` | `addAudiobookChapter` | Thêm một chương dạng kịch bản, hoặc nối tiếp chương chưa bắt đầu (`appendTo`) |
| `POST /v1/audiobooks/{bookId}/start` | `startAudiobook` | Đưa vào hàng chờ; `402` khi các gói không đủ trả — tổng không đủ, hoặc có chương không gói nào trả được, vì mỗi chương do một gói trả trọn. Cũng dùng để chạy tiếp và tạo lại chương lỗi |
| `POST /v1/audiobooks/{bookId}/cancel` | `cancelAudiobook` | Dừng. Chương chưa bắt đầu không bao giờ bị tính tiền |

### ID giọng khác nhau theo engine {#voice-ids-differ-by-engine}

Mỗi giọng thuộc về đúng một engine, và từ **2026-09-24** Cloud API chỉ còn đúng
một engine: `v4`, với id là tên hiển thị tiếng Việt — `Ngọc Lan`, chứ không phải
slug. Danh mục `v3` đã ngừng dùng slug `vieneu-…` khó đọc, và hai danh mục gần
như không chung id nào, nên một giọng chép từ danh sách `v3` cũ giờ không tra ra
ở đâu nữa. Nó bị từ chối với `400` chứ không âm thầm ánh xạ sang một giọng `v4`
— và đó giờ là nguyên nhân phổ biến nhất của các lỗi `400` trên những route tổng
hợp giọng nói, với những bên gọi đã hard-code giọng từ trước ngày ngừng, đẩy lên
production, rồi chẳng có lý do gì để nhìn lại cái id đó.

**Hãy tra id lúc chạy, giới hạn theo đúng engine bạn sắp dùng:**

```bash
curl -s "https://api.vieneu.io/api/v1/voices?engine=v4"
```

Không cần key. `GET /v1/voices` không lọc cũng trả về đúng danh mục `v4` đó
(cộng thêm các bản nhân bản `v4` của chính bạn khi gửi kèm key); `?engine=v3`
trả `400` kèm thông báo ngừng engine dẫn ở phần [Engine](#engines).

## Engine {#engines}

Request có thể mang thêm `engine`. Từ **08:00 ngày 2026-09-24 (GMT+7)**, giá trị
duy nhất Cloud API còn nhận là `v4`, cũng chính là mặc định, nên bạn có thể bỏ
hẳn trường này. `v3` đã ngừng phục vụ từ sáng hôm đó: bất kỳ request nào nêu nó
— trên `POST /v1/tts`, `/v1/tts/stream`, `/v1/audio/speech`, `/v1/dialogue`,
`/v1/dub`, `/v1/srt`, hay dưới dạng `GET /v1/voices?engine=v3` — đều bị từ chối
với `400` trước khi bất cứ khoản nào bị tính, và thông báo nói rõ lý do:

```
Engine "v3" was retired on 2026-09-24. Use engine "v4" and a voice from GET /v1/voices?engine=v4 — the two catalogues share no ids.
```

Nếu bạn từng ghim `v3`, cách sửa gồm ba phần: gửi `"engine": "v4"` (hoặc không
gửi gì), lấy một voice id từ `GET /v1/voices?engine=v4` thay vì dùng lại id cũ,
và đăng ký lại trên [Studio](https://vieneu.io/#/clone) bất kỳ bản nhân bản nào
bạn đã tạo trên `v3`. Audio bạn đã tạo trên `v3` không bị ảnh hưởng — các mục
trong thư viện và link tải vẫn hoạt động.

Engine tính phí theo hệ số riêng, nhân lên trên đơn giá mỗi ký tự:

| Engine | Hệ số |
|---|---|
| `v4` | 3× |

Con số này đặt theo từng bản triển khai và có thể thay đổi, nên bảng trên là
ảnh chụp một thời điểm, không phải cam kết. **`GET /v1/engines` trả về giá trị
đang chạy** — không cần API key. Nếu bạn đang dự trù chi phí cho một khối lượng
lớn, hãy đọc từ đó thay vì từ trang này:

```bash
curl -s https://api.vieneu.io/api/v1/engines
```

```json
{ "globalMultiplier": 1.3,
  "engines": [
  { "key": "v4", "isDefault": true, "sampleRate": 48000, "billingMultiplier": 3,
    "features": ["generate", "clone", "dialogue", "stream", "dub", "srt"] }
] }
```

### Hệ số thứ hai

`globalMultiplier` trong phản hồi đó là một đòn bẩy toàn nền tảng, nằm **trên**
hệ số của từng engine. Giá đầy đủ của một yêu cầu là:

```
token = ký tự × hệ số engine × globalMultiplier
```

Nó thường nằm ở `1` suốt những quãng dài — chính vì thế mà rất dễ bỏ sót, và cũng
chính vì thế mà nó được trả về ngay cạnh hệ số engine chứ không nằm ở một trang
tài liệu khác. Tại thời điểm viết, nó là `1.3`. Dự trù chi phí chỉ theo hệ số
engine thì con số của bạn sẽ thiếu đúng bằng nó trên bất kỳ bản triển khai nào
đã chỉnh đòn bẩy này.

(Còn một hệ số thứ ba — phụ phí tinh chỉnh văn bản bằng AI — chỉ áp cho những
yêu cầu có bật bước đó, không thuộc giá gốc.)

`features` cũng là cách bạn kiểm tra bằng code xem một engine làm được gì —
`stream` có trong danh sách của `v4`, nên mọi stream đều chạy ở đó và tính phí
theo hệ số `v4`, giờ cũng là hệ số của mọi lời gọi khác. Mỗi giọng thuộc về đúng
một engine — `GET /v1/voices?engine=v4` liệt kê danh mục của engine đó. Truyền
một giọng thuộc danh mục `v3` đã ngừng sẽ bị từ chối chứ không âm thầm thay bằng
giọng khác.

**Streaming chỉ có trên `v4`**, và từ trước khi ngừng `v3` đã vậy rồi, vì một lý
do đáng nhớ nếu có lúc bạn đem so các nhà cung cấp: `v3` chưa bao giờ thực sự
stream — nó dồn hết các chunk ra một lượt ở cuối — nên `POST /v1/tts/stream` từ
chối nó thay vì hứa một mức time-to-first-audio mà nó không giữ nổi. Giờ `v3` đã
rút khỏi mọi route thì cái chốt đó không còn ý nghĩa: một request stream nêu `v3`
nhận đúng lỗi `400` ngừng engine như mọi request khác, và `POST /v1/audio/speech`
kèm `"model": "vieneu-v3"` cũng vậy — trước 2026-09-24 route này vẫn nhận, trả
200, rồi dồn cả cụm chunk ra ở cuối mà không có gì trong phản hồi nói cho bạn
biết. Nếu bạn ghim engine cho một client stream, hãy ghim `v4` — xem
[Cắm vào app nói giao thức OpenAI](../integrations/openai-clients.md).

Giọng nhân bản cũng stream được, miễn là nó được đăng ký trên `v4` — mọi bản
nhân bản tạo từ 2026-08-28 đều như vậy, còn bản cũ đăng ký trên `v3` thì không
còn dựng tiếng được nữa cho tới khi đăng ký lại. Xem
[Giọng nhân bản](./streaming.md#cloned-voices).

## Tính phí {#billing}

Việc tổng hợp giọng nói tính phí theo **ký tự gửi lên**, nên chi phí của một lời
gọi có thể suy ra từ chính request, trước khi bạn gửi đi. Tối thiểu 50 ký tự mỗi
request. Request không tạo ra được audio sẽ được hoàn tiền tự động.

`aiRefine` mặc định **tắt** trên API. Khi tắt, text của bạn được tổng hợp đúng y
như lúc gửi. Bật lên (`"aiRefine": true`) để dùng lượt xử lý AI mà web app đang
dùng — công thức, từ viết tắt và tiếng Anh chen vào đều đọc đúng, và nội dung được
kiểm duyệt — đổi lại là một khoản phụ phí và thêm một round-trip nữa vào độ trễ.

Bước chuẩn bị văn bản tất định vẫn chạy trong cả hai trường hợp, nên tiếng Việt
vẫn được phát âm đúng dù bật hay tắt.

**Còn bao nhiêu token?** `GET /v1/balance` trả `remaining`: số token một lời gọi
tiêu được ngay lúc này. Mỗi lời gọi do một gói trả — gói của key nếu gói đó trả
được, nếu không thì một gói khác còn hoạt động của tài khoản — nên đây là chỗ
trống nhiều nhất của một gói: token còn lại của gói, hoặc ít hơn nếu hạn mức
ngày/tuần thấp hơn. `plan` là gói mà con số đó đo trên, có tổng, phần còn lại của
từng hạn mức kèm giờ làm mới (`null` khi không có hạn mức đó) và ngày hết hạn.
`spendableTokens` là tổng đang chạy: mọi gói trả được còn tiêu được bao nhiêu, mỗi
gói tính tới hạn mức của riêng nó, cộng lại — giảm đúng bằng chi phí mỗi lời gọi
dù gói nào trả, và bằng `remaining` khi chỉ một gói trả hết. `totalRemainingTokens`
cộng mọi gói còn hạn của tài khoản. Một lần tổng hợp cần ít nhất
`50 × hệ số engine` token.

### Endpoint duy nhất không tính phí theo ký tự {#the-one-endpoint-that-is-not-billed-per-character}

`POST /v1/dub` tính theo **độ dài audio đầu ra** — 65 token mỗi giây, sàn 150
token — và **chỉ trừ khi thành công**. Lồng tiếng chạy nhận dạng giọng nói trước
khi tổng hợp, nên khối lượng công việc bám theo bản ghi dài bao nhiêu, chứ không
theo việc người nói nhét được bao nhiêu ký tự vào đó. Hệ số engine vẫn được nhân
thêm lên trên đó.

Vậy nên dub là lời gọi duy nhất mà bạn không thể tính chi phí từ request body
trước khi gửi. Mỗi lần tải lên đều được kiểm tra khả năng chi trả theo chính độ
dài của nó trước khi cho qua, nên một request bạn không đủ tiền trả sẽ bị từ chối
trước khi tốn bất kỳ giây GPU nào.

## Hạn mức và request id {#rate-limits-and-request-ids}

Có **hai** bộ giới hạn đứng trước bạn, và chúng đếm hai thứ khác nhau. Một hệ tích
hợp chỉ biết một trong hai sẽ hiểu sai một nửa số lỗi 429 của mình.

**Bộ giới hạn của ứng dụng đếm theo API key của bạn.** Đây là bộ khớp với thứ bạn
đã mua: nó tính theo chính cái key, nên rải lời gọi ra nhiều máy cũng không được
thêm, và dùng chung mạng văn phòng cũng không mất phần. Hạn mức đặt theo từng
route, và mọi route trên `/v1` đều tự đặt mức riêng:

| Route | Mỗi phút |
|---|---|
| `/v1/audio/speech`, `/v1/tts`, `/v1/tts/stream`, `/v1/vapi/speech` | 300 |
| `/v1/dialogue` | 20 |
| `/v1/dub`, `/v1/srt` | 10 |
| Quản lý webhook | 10–30, tuỳ operation |
| `GET /v1/voices`, `/v1/audio/voices`, `/v1/engines`, `/v1/emotion-tags`, `GET /v1/tts/{jobId}` | không bị throttle |

Khi bộ giới hạn này từ chối bạn, nó nói rõ ra:

```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 43
Retry-After: 43
X-Request-Id: 0f7c…
```

`X-RateLimit-Reset` là lúc cửa sổ đếm lật sang chu kỳ mới; `Retry-After` là lúc
bạn được phép gọi lại. Thường thì hai giá trị trùng nhau, và khi không trùng, hãy
theo `Retry-After`.

**Proxy ở biên đếm theo địa chỉ nguồn của bạn.** Đứng trước ứng dụng là nginx,
thứ chỉ có thể tính theo IP. Trên `/api/v1`, nó cho phép **3000 request mỗi phút
và 300 kết nối đồng thời cho mỗi địa chỉ**. Mức đó lỏng có chủ đích — nó là lớp
đệm chống DDoS, không phải hạn mức của sản phẩm — nhưng nó có thật, nó **dùng
chung với mọi người gọi khác đứng sau cùng địa chỉ của bạn**, và nó không biết gì
về key của bạn. Một khách hàng gọi từ chục cái máy thì ở đó trông như chục khách
hàng, còn chục khách hàng đứng sau IP đi ra của một nền tảng tích hợp thì trông
như một.

**Dấu hiệu nhận biết nằm ở header.** Một lỗi `429` có kèm `X-RateLimit-*` là từ
ứng dụng và nói về key của bạn. Một lỗi `429` hoặc `503` trơ trọi, không có những
header đó và cũng không có `Retry-After`, là từ biên và nói về IP của bạn — nginx
trả về trang riêng của nó, không kèm header nào của chúng tôi. Khác biệt đó quyết
định bạn phải làm gì: trường hợp thứ nhất, giãn nhịp theo key; trường hợp thứ hai,
giãn nhịp *và* xem có bao nhiêu máy đang dùng chung địa chỉ đi ra đó, vì nới thêm
số kết nối đồng thời chỉ làm mọi thứ tệ hơn.

Streaming là chỗ điều này quan trọng nhất, vì `/v1/tts/stream` khớp một khối
`location` nginx khác với phần còn lại của `/api/v1` và thừa hưởng một trần kết
nối chặt hơn nhiều — xem [Hạn mức](./streaming.md#limits).

Mọi response, thành công hay thất bại, đều mang `X-Request-Id`. Hãy dẫn nó ra khi
gửi yêu cầu hỗ trợ — log của chúng tôi được đánh chỉ mục theo chính id đó.

## Lỗi {#errors}

Các endpoint gốc trả về cấu trúc quen thuộc:

```json
{ "statusCode": 403, "message": "Insufficient tokens", "traceId": "0f7c…" }
```

Riêng `/v1/audio/speech` trả về cấu trúc của OpenAI, để client OpenAI parse được.

| Status | Ý nghĩa |
|---|---|
| 400 | Request sai định dạng — giọng không tồn tại, format không hợp lệ, hoặc text bị bộ kiểm tra từ chối |
| 401 | API key thiếu, sai định dạng hoặc đã bị thu hồi |
| 403 | Hết token, grant đã hết hạn, hoặc gói của bạn không bao gồm engine này |
| 422 | Nội dung bị bộ kiểm duyệt từ chối (chỉ xảy ra khi bật `aiRefine`) |
| 429 | Chạm hạn mức hoặc hết hạn ngạch token — xem phần trên để phân biệt |
| 503 | Không có worker rảnh cho engine hoặc format được yêu cầu; thử lại sau chốc lát |

Lỗi nào có kèm `code` máy đọc được, lỗi nào không, và với mỗi loại thì phải làm
gì — tất cả nằm ở trang [Lỗi](./errors.md).
