📘 Tài liệu API
Chuyển văn bản thành giọng nói qua API. Tạo key ở API Keys — key chỉ hiện 1 lần, lưu lại ngay.
Quick Start — 3 bước đơn giản
1 · Tạo API Key
Vào API Keys để tạo key (dạng sk_live_...).
2 · Lấy Voice ID
curl https://voice.getpekka.com/api/v1/voices \
-H "Authorization: Bearer YOUR_API_KEY"3 · Tạo TTS job rồi poll kết quả
curl -X POST https://voice.getpekka.com/api/v1/tts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Xin chào, đây là giọng nói AI",
"voiceId": "YOUR_VOICE_ID",
"speed": 1.0
}'🤖 Tích hợp bằng AI — copy prompt này
Dán prompt này vào ChatGPT/Claude/Cursor để trợ lý tự viết code tích hợp. Nhớ thay <API_KEY> bằng key thật.
Bạn hãy viết code tích hợp API chuyển văn bản thành giọng nói (TTS) của Pekka Voice cho dự án của tôi. Dưới đây là toàn bộ đặc tả, hãy đọc kỹ rồi hỏi tôi ngôn ngữ/framework nếu chưa rõ.
## Xác thực
Mọi request cần header:
Authorization: Bearer <API_KEY>
API key dạng `sk_live_...`, lấy ở https://voice.getpekka.com/docs/api-keys
Base URL: https://voice.getpekka.com
## 1. Tạo audio — POST /api/v1/tts (dùng cho văn bản dài, ĐÂY LÀ CÁCH NÊN DÙNG)
Body JSON:
text string — văn bản cần đọc, tối đa 400.000 ký tự
srt string — HOẶC nội dung file phụ đề .srt/.vtt (tối đa 2MB).
Gửi text HOẶC srt, gửi cả hai sẽ bị từ chối 400.
Chỉ phần chữ được đọc; timecode và thẻ định dạng bị bỏ.
voiceId string — bắt buộc, lấy từ GET /api/v1/voices
speed number — 0.8 đến 2.0, mặc định 1.0
pauseMs number — nghỉ giữa câu (ms), 0 đến 15000, mặc định 100
dictionary boolean — mặc định true. Áp từ điển thay thế của tài khoản:
đọc "AI" thành "ây ai", "UBND" thành "ủy ban nhân
dân"... Quản lý danh sách từ trong giao diện web.
Gửi false nếu muốn đọc nguyên văn không thay thế.
Response 200 (state="completed"):
{ projectExportId, state, url, mp3Url, srtUrl, characterCount,
blockCount, cueCount?, creditsCharged, error }
- url : audio gốc, luôn có khi completed. Đây là nguồn chính, dùng cái này
- mp3Url : bản MP3 nhẹ hơn, best-effort — CÓ THỂ null dù đã completed
- srtUrl : file phụ đề khớp audio vừa tạo. LƯU Ý timeline này sinh từ nhịp
đọc thật của giọng, KHÔNG phải timeline trong file srt gửi lên.
- cueCount: chỉ có khi gửi bằng `srt` — số cue đọc được. Hãy so với số cue
trong file của tôi để phát hiện file hỏng một phần.
## 2. Xử lý status code — RẤT QUAN TRỌNG, đừng làm sai chỗ này
200 — xong, dùng luôn `url`.
202 — state là "pending"/"processing". ĐÂY KHÔNG PHẢI LỖI. Văn bản dài nên
chưa xong trong một lần gọi. Hãy POLL `GET /api/v1/exports/{projectExportId}`
mỗi 2–3 giây cho tới khi state = "completed". Mỗi lần poll cũng đẩy job
tiến lên, nên ngừng poll là job đứng.
TUYỆT ĐỐI KHÔNG gọi lại POST /api/v1/tts để "thử lại" — tạo job mới và tính tiền lần nữa.
400 — sai tham số (đọc field `error`).
401 — API key sai hoặc thiếu.
402 — không đủ credit. Response có creditsNeeded, creditsRemaining.
413 — body vượt 3MB.
429 — vượt rate limit theo PHÚT (cửa sổ cố định 1 phút cho mỗi key). Đợi
đến phút kế tiếp rồi thử lại, đừng retry liên tục. Giới hạn theo gói:
Free 10, Starter 30, Pro 60, Business 300 request/phút.
413 — body vượt 3MB.
500 — state="failed", đây mới là lỗi thật sự cuối cùng.
503 — hệ thống tạm lỗi kết nối cơ sở dữ liệu. Đợi theo header Retry-After
(thường 60 giây) rồi thử lại. Không coi đây là lỗi vĩnh viễn.
## 3. Văn bản ngắn — POST /api/v1/tts/sync
Trả audio ngay trong response, không cần poll. CHỈ dùng cho văn bản tối đa
1500 ký tự. Body: { text, voiceId, speed }. Trường `url` trả về là đường dẫn
tương đối (/api/audio/...) — ghép với https://voice.getpekka.com trước khi dùng.
Dùng khi cần độ trễ thấp (chatbot, thông báo).
QUAN TRỌNG: văn bản dài hơn 1500 ký tự thì dùng POST /api/v1/tts (mục 1), KHÔNG
tự cắt nhỏ rồi gọi /tts/sync song song. Cách đó tốn nhiều request hơn, dễ chạm
rate limit theo phút, và không có file ghép + phụ đề SRT.
## 4. Danh sách giọng — GET /api/v1/voices
## 5. Kiểm tra credit — GET /api/v1/usage
Trả { plan, creditBalance, accountBalance, keyRemaining, limitedBy, creditLimit,
creditsUsed, creditsPerMonth, ... }. creditBalance = min(accountBalance,
keyRemaining) — số thực sự tiêu được; limitedBy nói trần nào đang chặn
("account" = pool chung hết, "key" = hạn mức riêng của key hết).
## Cách tính tiền
1 credit = 1 ký tự (tính trên văn bản SAU khi bóc phụ đề, nên timecode không
mất tiền). Nhóm giọng tiết kiệm chỉ tính 1/3. Câu đã tạo rồi mà tạo lại y hệt
thì dùng cache, không trừ thêm.
## Yêu cầu với code bạn viết
- Đọc API key từ biến môi trường, KHÔNG hardcode.
- Có hàm poll cho trường hợp 202: poll mỗi 2–3 giây, có tổng thời gian chờ tối đa để không treo vô hạn.
- Link audio (url, mp3Url, srtUrl) là link công khai, KHÔNG cần header Authorization để tải.
- Xử lý riêng từng status code ở trên, thông báo lỗi rõ ràng bằng tiếng Việt.
- Có timeout hợp lý cho HTTP request (job dài có thể mất vài phút).
- Viết kèm một ví dụ chạy được ngay.POST /api/v1/ttsRecommended
Văn bản dài → 1 file audio + phụ đề SRT. Trả thẳng url + srtUrl khi xong. Văn bản rất dài trả về HTTP 202 (state: "pending", không phải lỗi) — dùng projectExportId poll GET /api/v1/exports/{id} tới khi completed, đừng gọi lại POST /api/v1/tts để "thử lại" (tạo job mới, không tiếp tục job cũ). Chỉ HTTP 500 mới là lỗi thật.
Tham số:
text(string) — tối đa 400.000 ký tự. Bắt buộc trừ khi dùngsrt.srt(string) — nội dung file.srt/.vtt, tối đa 2MB, thay chotext(gửi cả hai sẽ bị từ chối). Chỉ tính tiền phần chữ, bỏ timecode.voiceId(string, bắt buộc) — lấy từ danh sách giọng.speed(number) — 0.8–2.0, mặc định 1.0.pauseMs(number) — nghỉ giữa câu (ms), 0–15000, mặc định 100.dictionary(boolean, mặc địnhtrue) — áp từ điển thay thế của tài khoản. Gửifalseđể đọc đúng nguyên văn.
Gửi bằng file phụ đề:
curl -X POST https://voice.getpekka.com/api/v1/tts \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile srt phude.srt --arg v YOUR_VOICE_ID '{srt: $srt, voiceId: $v}')"Response có thêm cueCount (số cue đọc được — đối chiếu để phát hiện file hỏng một phần). srtUrl trả về là timeline mới sinh từ nhịp đọc thật, không phải timeline gốc bạn gửi lên.
url là audio gốc, luôn có khi completed — dùng làm nguồn chính. mp3Url là bản MP3 nhẹ hơn, sinh sau — best-effort, có thể vẫn null dù đã completed.
Response:
{
"projectExportId": "3f6c1c9a-...",
"state": "completed",
"url": "https://.../api/audio/<hash>",
"mp3Url": "https://.../api/audio/<hash>", // null nếu chưa/không có bản MP3
"srtUrl": "https://.../api/exports/<id>/srt",
"characterCount": 203,
"blockCount": 3,
"creditsCharged": 203,
"error": null
}GET /api/v1/exports/{id}
Kiểm tra tiến trình, lấy URL audio + phụ đề khi xong. Poll mỗi 2–3 giây.
{
"projectExportId": "3f6c1c9a-...",
"state": "completed", // pending | processing | completed | failed
"url": "https://.../api/audio/<hash>",
"mp3Url": "https://.../api/audio/<hash>", // null nếu chưa/không có bản MP3
"srtUrl": "https://.../api/exports/<id>/srt",
"characterCount": 203,
"blocksReady": 480, // chỉ có khi state pending/processing — số câu ĐÃ tạo xong audio
"blocksTotal": 621, // (kể cả khi characterCount/blockCount còn 0 vì bước ghép cuối chưa chạy)
"creditsCharged": 203,
"error": null
}Văn bản dài: characterCount/blockCount chỉ có ở bước ghép cuối — trong lúc còn pending, xem tiến độ thật qua blocksReady/blocksTotal.
Ví dụ polling bằng Node.js:
async function waitForAudio(exportId) {
while (true) {
const res = await fetch(
`https://voice.getpekka.com/api/v1/exports/${exportId}`,
{ headers: { Authorization: "Bearer YOUR_API_KEY" } }
)
const job = await res.json()
if (job.state === "completed") return job // job.url + job.srtUrl
if (job.state === "failed") throw new Error(job.error)
await new Promise((r) => setTimeout(r, 2500)) // đợi 2.5s rồi thử lại
}
}POST /api/v1/tts/sync
Văn bản ngắn (≤ 1.500 ký tự) — trả URL audio ngay, không cần poll. Body: { text, voiceId, speed? }.
{
"url": "/api/audio/<hash>",
"creditsUsed": 42,
"creditsRemaining": 2958
}GET /api/v1/voices
Danh sách giọng — lấy id để dùng làm voiceId ở 2 endpoint TTS phía trên.
Query params (không bắt buộc):
page(number) — mặc định 1.limit(number) — số giọng mỗi trang, mặc định 20.
curl "https://voice.getpekka.com/api/v1/voices?page=1&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"Response:
{
"items": [
{
"id": "vi-VN-HoaiMyNeural",
"name": "Hoài My",
"description": "Giọng nữ miền Bắc, ấm áp",
"tags": ["nữ", "miền Bắc"],
"economy": true,
"free": false,
"charsPerCredit": 3
}
],
"total": 87,
"hasNext": true,
"page": 1
}economy: true = giọng rẻ hơn, free: true = 0 credit. Lặp page tới khi hasNext là false để lấy hết danh sách.
GET /api/v1/usage
Số dư của CHÍNH API key đang gọi. creditBalance là số thực sự tiêu được, tức phần nhỏ hơn giữa HAI trần: số dư chung của tài khoản (accountBalance) và phần còn lại của riêng key này (keyRemaining, null khi key không đặt hạn mức riêng). limitedBy cho biết trần nào đang chặn — "account" nghĩa là chủ tài khoản phải nạp thêm, tăng hạn mức key cũng không giúp gì.
{
"plan": "pro",
"creditBalance": 412350,
"accountBalance": 412350,
"keyRemaining": 500000,
"limitedBy": "account",
"creditLimit": 500000,
"creditsUsed": 87650,
"creditsExpireAt": "2026-08-16T00:00:00.000Z",
"creditsPerMonth": 1000000
}Giới hạn & lỗi
- Rate limit theo gói: Free 10 · Starter 30 · Pro 60 · Business 300 request/phút.
- Credits dùng chung web app + API, tỉ lệ ký tự/credit khác nhau theo giọng — xem
charsPerCredittrongGET /api/v1/voices. - Mỗi lần gọi đều trừ credit theo độ dài văn bản, kể cả gọi lại nội dung y hệt.
- Mỗi API key có thể đặt hạn mức riêng (quản lý ở /docs/api-keys) — hết hạn mức riêng báo
402dù tài khoản còn credit. 401key sai/thiếu ·402hết credits ·413body quá 3MB ·429vượt rate limit (tính theo phút) ·500lỗi hệ thống ·503cơ sở dữ liệu tạm lỗi — đợi theoRetry-Afterrồi thử lại.
Best Practices
- Polling: mỗi 2–3 giây; văn bản ngắn thường xử lý trong vài giây.
- Bảo mật: không bao giờ expose API key trong code frontend — luôn gọi từ backend.
- Error handling: luôn check
statetrước khi dùngurl; nếufailed, xemcreditsCharged: bằng0nghĩa là credit đã được hoàn lại.