Tổng quan Cloud API
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.
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 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 đủ
Mọi endpoint, mọi field, mọi lỗi đều nằm trong 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 — 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:
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
https://api.vieneu.io/api/v1
Xác thực
Mọi request đều mang API key của bạn, đặt kiểu nào cũng được:
-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 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
Đồng bộ — bạn nhận thẳng các byte audio trong response:
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 — đườ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ó:
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.
Toàn bộ 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 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 — 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, 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.
Webhooks — xem Webhooks để 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.
| 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
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:
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.
Engine
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 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:
curl -s https://api.vieneu.io/api/v1/engines
{ "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.
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.
Tính phí
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ự
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
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.
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
Các endpoint gốc trả về cấu trúc quen thuộc:
{ "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.