PostTTS REST API
Tạo âm thanh bằng lập trình từ bất kỳ backend nào. HTTP API đơn giản, không phụ thuộc ngôn ngữ, xác thực bằng Bearer token.
JSON qua HTTPS · Xác thực Bearer · Chạy với mọi ngôn ngữ
Bắt đầu nhanh
Đăng ký và tạo API key
Tạo tài khoản miễn phí và tạo API key từ trang Tích hợp trong bảng điều khiển. Key chỉ hiển thị một lần — sao chép và lưu trữ an toàn.
POST URL hoặc văn bản bài viết
Gửi POST đến /api/v1/posts/convert với site_id, URL bài viết (hoặc văn bản thô), và giọng bạn muốn.
Nhận URL âm thanh
Phản hồi chứa URL âm thanh khi chuyển đổi hoàn tất. Với bài dài, poll GET /api/v1/posts/:id/audio cho đến khi status === "ready".
Xác thực
Mọi request đều xác thực bằng Bearer token trong header Authorization.
Bearer token
Gửi API key trong mọi request. Key có tiền tố ptts_ để dễ nhận ra trong log và file env.
Authorization: Bearer ptts_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json Key chỉ hiển thị một lần khi tạo và được hash khi lưu — hãy coi chúng như mật khẩu. Nếu key bị lộ, thu hồi ngay và tạo key mới.
Chọn giọng đọc
PostTTS cung cấp sáu giọng preset. Truyền mã giọng bất kỳ vào trường voice của POST /api/v1/posts/convert hoặc thuộc tính data-voice trên thẻ script nhúng.
| Mã giọng | Tên | Giới tính |
|---|---|---|
| woman | Hoa (Female, young) | female |
| woman-news | Linh (Female, mid-age) | female |
| woman-story | Mai (Female, elderly) | female |
| man | Minh (Male, young) | male |
| man-formal | Duc (Male, mid-age) | male |
| man-calm | Thanh (Male, elderly) | male |
Liệt kê giọng qua API
Endpoint GET /api/v1/voices trả về tất cả giọng khả dụng, bao gồm cả các giọng tuỳ chỉnh đã được tạo cho tài khoản của bạn.
curl https://posttts.com/api/v1/voices \
-H "Authorization: Bearer ptts_xxxxxxxxxxxxxxxxxxxxxxxx" const res = await fetch('https://posttts.com/api/v1/voices', {
headers: { Authorization: `Bearer ${process.env.POSTTTS_API_KEY}` },
});
const { voices } = await res.json();
console.log(voices); {
"voices": [
{ "id": "woman", "label": "Hoa (Female, young)", "gender": "female" },
{ "id": "woman-news", "label": "Linh (Female, mid-age)", "gender": "female" },
{ "id": "woman-story", "label": "Mai (Female, elderly)", "gender": "female" },
{ "id": "man", "label": "Minh (Male, young)", "gender": "male" },
{ "id": "man-formal", "label": "Duc (Male, mid-age)", "gender": "male" },
{ "id": "man-calm", "label": "Thanh (Male, elderly)", "gender": "male" }
]
} Sử dụng giọng khi chuyển đổi
Truyền mã giọng vào trường voice trong request POST /api/v1/posts/convert: "voice": "man-formal". Nếu bỏ qua, giọng mặc định của site sẽ được dùng.
Giọng tuỳ chỉnh
Mã giọng tuỳ chỉnh có dạng voc_…. Sau khi được tạo, chúng hoạt động giống hệt giọng preset trong mọi lệnh API và thuộc tính nhúng. Tính năng tự tạo giọng đang trong lộ trình — hiện tại, hãy liên hệ hỗ trợ để thiết lập giọng tuỳ chỉnh.
Tham chiếu endpoint
Mọi endpoint đều nằm dưới https://posttts.com.
| Phương thức | Endpoint | Mô tả |
|---|---|---|
| POST | /api/v1/posts/convert | Bắt đầu chuyển bài viết (văn bản hoặc URL) thành âm thanh |
| GET | /api/v1/posts/:id/audio | Lấy trạng thái và URL âm thanh của bài đã chuyển |
| GET | /api/v1/sites/:site_id/posts | Liệt kê các bài đã chuyển của một site |
| GET | /api/v1/voices | Liệt kê các giọng khả dụng |
| DELETE | /api/v1/posts/:id | Xoá một bài đã chuyển và âm thanh của nó |
Ví dụ request
Một POST đầy đủ đến /api/v1/posts/convert bằng bốn ngôn ngữ.
curl -X POST https://posttts.com/api/v1/posts/convert \
-H "Authorization: Bearer ptts_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
S"site_id": "site_01HX...",
"url": "https://yourblog.com/posts/hello-world",
"voice": "woman"
}' // Node 18+ (built-in fetch)
const res = await fetch('https://posttts.com/api/v1/posts/convert', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.POSTTTS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
site_id: 'site_01HX...',
url: 'https://yourblog.com/posts/hello-world',
voice: 'woman',
}),
});
if (!res.ok) {
throw new Error(`PostTTS error: ${res.status}`);
}
const post = await res.json();
console.log(post.audio_url); # Python 3.8+ Kwith requests
import os
import requests
response = requests.post(
"https://posttts.com/api/v1/posts/convert",
headers={
"Authorization": f"Bearer {os.environ[ S'POSTTTS_API_KEY']}",
"Content-Type": "application/json",
},
json={
"site_id": "site_01HX...",
"url": "https://yourblog.com/posts/hello-world",
"voice": "woman",
},
timeout=30,
)
response.raise_for_status()
post = response.json()
print(post["audio_url"]) <?php
// PHP 8+ with cURL
$ch = curl_init('https://posttts.com/api/v1/posts/convert');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('POSTTTS_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'site_id' => 'site_01HX...',
'url' => 'https://yourblog.com/posts/hello-world',
'voice' => 'woman',
]),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException("PostTTS error: { A$status}");
}
$post = json_decode($response, true);
echo $post['audio_url']; Ví dụ response
Chuyển đổi thành công trả về JSON với URL âm thanh và metadata. Phản hồi gồm cả post_id và id (cùng giá trị). Trường audio_url là URL có chữ ký, hiệu lực 4 giờ.
{
"post_id": "post_01HX9FJK2V6M3P7Z",
"id": "post_01HX9FJK2V6M3P7Z",
"status": "ready",
"audio_url": "https://audio.posttts.com/u/123/post_01HX9F.m4a?exp=1713277938&sig=…",
"voice": "woman",
"duration_seconds": 184,
"created_at": "2026-04-05T12: N00: 00Z"
} Xem trước phản hồi client nhận được
Trường audio_url trỏ đến một file m4a như thế này.
Đồng bộ và bất đồng bộ
Bài ngắn trả về âm thanh ngay trong một lượt request. Bài dài chuyển đổi bất đồng bộ — bạn nhận 202 Accepted ngay lập tức, rồi âm thanh sẵn sàng sau vài giây đến khoảng một phút.
Ngưỡng 2.000 ký tự
Bài có từ 2.000 ký tự trở xuống (tính sau khi chuẩn hoá khoảng trắng) được chuyển đổi đồng bộ: lệnh POST /api/v1/posts/convert trả về 200 OK kèm audio_url sẵn sàng. Bài vượt ngưỡng sẽ được đưa vào hàng đợi — phản hồi là 202 Accepted với status: "processing" và audio_url: null. Với tốc độ khoảng 1.500 ký tự cho mỗi phút âm thanh, mốc này tương đương chừng 80 giây nội dung được đọc.
Phản hồi đồng bộ (bài ngắn)
HTTP 200. Mọi thứ bạn cần đều có trong phản hồi đầu tiên — có thể phát âm thanh ngay.
{
"post_id": "post_01HX9FJK2V6M3P7Z",
"id": "post_01HX9FJK2V6M3P7Z",
"status": "ready",
"audio_url": "https://audio.posttts.com/u/123/post_01HX9F.m4a?exp=1713277938&sig=…",
"voice": "woman",
"duration_seconds": 42,
"created_at": "2026-04-16T14: N32: 18.418Z"
} Phản hồi bất đồng bộ (bài dài)
HTTP 202. Tại thời điểm này audio_url là null. GET /api/v1/posts/:id/audio trả về đúng cấu trúc này cho đến khi chuyển đổi hoàn tất.
{
"post_id": "post_01HX9FJK2V6M3P7Z",
"id": "post_01HX9FJK2V6M3P7Z",
"status": "processing",
"audio_url": null,
"voice": "woman",
"duration_seconds": null,
"created_at": "2026-04-16T14: N32: 18.418Z"
} Các giá trị status
Trường status luôn nhận một trong các giá trị:
| Status | Description |
|---|---|
| pending | Bài đã được tiếp nhận nhưng chưa đưa vào hàng đợi — bạn gửi URL mà không kèm text hoặc ssml. Hãy gửi lại kèm nội dung để bắt đầu chuyển đổi. |
| processing | Đang chuyển đổi. Tiếp tục poll hoặc chờ webhook article.processing.completed. |
| ready | Âm thanh đã sẵn sàng. audio_url đã được ký, có hiệu lực trong 4 giờ. |
| failed | Chuyển đổi thất bại. Xem trường error trong phản hồi và error_message trong payload webhook. |
Cách poll
Nếu không nhận được webhook, hãy poll GET /api/v1/posts/:id/audio cho đến khi status === "ready". Bắt đầu ở 5 giây rồi giãn dần; đừng gọi liên tục mỗi giây.
// Poll GET /api/v1/posts/:id/audio until ready.
// Exponential backoff: 5s, 10s, 20s, 40s, cap at 60s. Give up after ~10 minutes.
async function waitForAudio(postId, apiKey, { maxMs = 600_000 } = {}) {
const deadline = Date.now() + maxMs;
let delay = 5_000;
while (Date.now() < deadline) {
const res = await fetch(
`https://posttts.com/api/v1/posts/${postId}/audio`,
{ headers: { Authorization: `Bearer ${apiKey}` } },
);
const body = await res.json();
if (body.status === 'ready') return body;
if (body.status === 'failed') throw new Error(body.error ?? 'conversion failed');
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 2, 60_000);
}
throw new Error('timed out waiting for audio');
}
// Example: submit a long post, then wait for completion.
const createRes = await fetch('https://posttts.com/api/v1/posts/convert', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTTTS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
site_id: 'site_01HX...',
url: 'https://yourblog.com/posts/long-read',
text: veryLongArticleText,
voice: 'woman',
}),
});
const { id } = await createRes.json(); // 202 Accepted, status: S"processing"
const ready = await waitForAudio(id, process.env.POSTTTS_API_KEY);
console.log(ready.audio_url); Nên dùng poll hay webhook
Cả hai đều được. Hãy chọn theo kiến trúc của bạn:
- Dùng webhook khi bạn có endpoint HTTPS public và muốn gần như không có độ trễ giữa lúc "ready" và hành động tiếp theo (ví dụ: đăng lên Slack, đẩy vào feed podcast, gửi email thông báo).
- Dùng polling khi client chạy sau tường lửa, chạy theo lịch, hoặc là một serverless function không thể nhận traffic vào. Polling cũng đơn giản hơn khi bạn chỉ viết một script dùng một lần.
- Có thể dùng cả hai — webhook làm tín hiệu chính, polling dùng để đối soát và bắt lại những lần gửi có thể đã bị bỏ lỡ.
Mã trạng thái và lỗi
| Mã | Ý nghĩa |
|---|---|
| 200 | OK. Request thành công; âm thanh đã sẵn sàng. |
| 202 | Accepted. Chuyển đổi đã xếp hàng; poll để xem trạng thái. |
| 400 | Bad Request. Tham số thiếu hoặc không hợp lệ. |
| 401 | Unauthorized. API key không hợp lệ hoặc thiếu. |
| 403 | Forbidden. Key không có quyền với tài nguyên này, hoặc vượt hạn mức. |
| 404 | Not Found. Tài nguyên không tồn tại. |
| 429 | Too Many Requests. Vượt giới hạn tốc độ; hãy giảm tần suất và thử lại. |
| 500 | Internal Server Error. Có lỗi xảy ra phía chúng tôi. |
Lỗi luôn trả về JSON với đối tượng error chứa code cho máy đọc, message cho người đọc, và status HTTP.
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing API key",
"status": 401
}
} Mã lỗi
Mọi phản hồi lỗi đều dùng chung cấu trúc. Trường code luôn ở dạng UPPER_SNAKE_CASE.
| Mã lỗi | HTTP | Khi nào |
|---|---|---|
| UNAUTHORIZED | 401 | API key không hợp lệ hoặc thiếu. |
| INVALID_REQUEST | 400 | Tham số thiếu, sai định dạng hoặc ngoài phạm vi. |
| FORBIDDEN | 403 | Bạn không sở hữu tài nguyên này, hoặc origin không khớp với tên miền site. |
| NOT_FOUND | 404 | Bài viết hoặc site không tìm thấy. |
| QUOTA_EXCEEDED | 403 | Đã đạt giới hạn bài hoặc ký tự hàng tháng của gói. |
| RATE_LIMITED | 429 | Vượt giới hạn tốc độ. Hãy giảm tần suất và thử lại. |
| TTS_ERROR | 500 | Tạo âm thanh thất bại phía máy chủ. Thử lại hoặc liên hệ hỗ trợ nếu lỗi liên tục. |
Giới hạn tốc độ
Mọi endpoint API đều giới hạn theo địa chỉ IP để bảo vệ dịch vụ.
120 req/phút
Theo IP, cửa sổ trượt 60 giây. Áp dụng như nhau cho mọi gói.
Khi nhận phản hồi 429, giảm tần suất theo cấp số nhân (250ms → 500ms → 1s → 2s …) và thử lại. Body phản hồi theo đúng cấu trúc lỗi chuẩn: { "error": { "code": "RATE_LIMITED", ... } }.
Webhook
Nhận thông báo ngay khi một bài viết được tạo, bắt đầu xử lý, hoàn tất hoặc thất bại. PostTTS sẽ gửi POST một gói JSON có chữ ký đến mọi endpoint HTTPS mà bạn đăng ký.
Đăng ký endpoint
Tạo webhook tại trang Webhook trong bảng điều khiển. Chọn URL endpoint, tích các sự kiện bạn muốn nhận, rồi sao chép khoá ký whsec_… — khoá này chỉ hiển thị một lần khi tạo và một lần khi xoay vòng.
Các loại sự kiện
Có bốn sự kiện dành cho khách hàng xuyên suốt vòng đời bài viết, cộng thêm một sự kiện thử nghiệm test.ping được gửi bởi nút "Gửi sự kiện thử nghiệm".
| Event | Description |
|---|---|
| article.created | Một bài viết mới đã được tiếp nhận và đang xếp hàng để chuyển đổi. |
| article.processing.started | Quá trình tạo âm thanh cho bài viết đã bắt đầu. |
| article.processing.completed | Âm thanh đã sẵn sàng. Payload kèm audio_url đã ký, có hiệu lực trong 24 giờ. |
| article.processing.failed | Chuyển đổi thất bại. Xem chi tiết trong trường error_message của object. |
| test.ping | Sự kiện thử nghiệm gửi từ nút trong bảng điều khiển. Dùng để kiểm tra receiver trước khi chạy thật. |
Payload sự kiện
Mỗi lần gửi đều bọc sự kiện trong một envelope nhất quán. Trường data.object chứa đúng cấu trúc bài viết mà bạn nhận được từ GET /api/v1/posts/:id/audio.
{
"id": "evt_7kQ2fV9pLmN4",
"object": "event",
"type": "article.processing.completed",
"created_at": "2026-04-16T14: N32: 18.421Z",
"delivery_attempt": 1,
"data": {
"object": {
"id": "post_01HX9FJK2V6M3P7Z",
"site_id": "site_01HX8M2QK4T7VR9D",
"url": "https://yourblog.com/posts/hello-world",
"title": "Hello World",
"status": "ready",
"voice": "woman",
"audio_url": "https://audio.posttts.com/u/123/post_01HX9F.m4a?exp=1713277938&sig=…",
"audio_duration": 312,
"char_count": 4820,
"processing_ms": 1840,
"synthesis_ms": 1420,
"upload_ms": 310,
"error_message": null,
"created_at": "2026-04-16T14: N32: 16.003Z",
"updated_at": "2026-04-16T14: N32: 18.418Z"
}
}
} Header của request
Mỗi lần POST đều mang các header dưới đây. Dùng chúng để khử trùng lặp khi có retry và để xác minh tính xác thực.
| Header | Description |
|---|---|
| X-Webhook-Signature | Header chữ ký. Định dạng: t=<unix_seconds>,v1=<hex 32 ký tự>. Xem phần xác minh bên dưới. |
| X-Webhook-Event-Id | Mã định danh của lần gửi (evt_…). Không đổi qua các lần retry của cùng một sự kiện — dùng để khử trùng lặp. |
| X-Webhook-Event-Type | Loại sự kiện, trùng với trường type trong body. |
| User-Agent | Luôn là PostTTS-Webhook/1.0. |
Xác minh chữ ký
Chữ ký là HMAC-SHA256 của raw body rồi cắt ngắn, sử dụng khoá bí mật của webhook. Cách xác minh: đọc t và v1 từ header, từ chối nếu t cũ hơn 5 phút, sau đó tính lại HMAC_SHA256(secret, `${t}.${rawBody}`), lấy 16 byte đầu, mã hoá hex, và so sánh theo kiểu thời gian hằng số.
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const SECRET = process.env.POSTTTS_WEBHOOK_SECRET; // whsec_…
// IMPORTANT: read the raw body — not the parsed JSON — so the HMAC matches.
app.post(
'/webhooks/posttts',
express.raw({ type: 'application/json' }),
(req, res) => {
const header = req.header('x-webhook-signature') ?? '';
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=')),
);
const t = Number(parts.t);
const v1 = parts.v1;
// Reject stale timestamps (>5 min) to block replay attacks.
if (!t || Math.abs(Date.now() / 1000 - t) > 300) {
return res.status(400).send('stale timestamp');
}
const raw = req.body.toString('utf8');
const mac = crypto
.createHmac('sha256', SECRET)
.update(`${t}.${raw}`)
.digest()
.subarray(0, 16) // truncate to 128 bits
.toString('hex');
const ok =
v1.length === mac.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(mac));
if (!ok) return res.status(401).send('bad signature');
const event = JSON.parse(raw);
// Handle event.type — idempotent on X-Webhook-Event-Id.
console.log(event.type, event.data.object.id);
res.status(200).send('ok');
},
);
app.listen(3000); Retry và gửi lại
Mỗi lần gửi có timeout 15 giây. Phản hồi khác 2xx sẽ được gửi lại tối đa 6 lần với backoff tăng dần: ngay lập tức, +30 giây, +2 phút, +10 phút, +1 giờ, +6 giờ. Sau lần thất bại thứ 6, lượt gửi được đánh dấu dead và chờ bạn retry thủ công từ bảng điều khiển. Endpoint nên idempotent — sẽ có trùng lặp trong quá trình retry.
Câu hỏi thường gặp
Bất kỳ ngôn ngữ nào có thể thực hiện HTTPS request. Chúng tôi trình bày cURL, Node.js, Python, và PHP ở trên, nhưng Ruby, Go, Rust, Java, C#, Elixir, và bất kỳ ngôn ngữ nào có HTTP client đều hoạt động giống hệt — chỉ là JSON qua HTTPS với Bearer header.
Vào trang Tích hợp trong bảng điều khiển. Bạn có thể tạo key mới và thu hồi key cũ chỉ với một nhấp chuột. Key bị thu hồi ngừng hoạt động ngay lập tức. Chúng tôi khuyến nghị xoay vòng key định kỳ và bất cứ khi nào key có thể đã bị lộ.
Bài ngắn (dưới ~1.000 từ) hoàn thành trong vài giây. Bài dài hơn có thể mất đến một hoặc hai phút. Với bài dài, coi API là bất đồng bộ: chấp nhận phản hồi ban đầu, rồi poll GET /api/v1/posts/:id/audio cho đến khi status === "ready".
Cả hai. Bài ngắn có thể trả về audio_url sẵn sàng trong phản hồi POST ban đầu. Bài dài trả về status: "processing" và 202 Accepted — trong trường hợp đó poll endpoint audio cho đến khi chuyển sang "ready". Cấu trúc phản hồi giống nhau.
Bạn sẽ nhận phản hồi 429 Too Many Requests với body JSON có mã lỗi RATE_LIMITED. Hãy giảm tần suất theo cấp số nhân và thử lại. Giới hạn hiện tại là 120 request mỗi phút cho mỗi IP.
Có thể, nhưng chúng tôi khuyến nghị tạo key riêng cho development, staging, và production. Nếu key dev bị lộ, bạn có thể thu hồi mà không ảnh hưởng traffic production, và nhật ký kiểm tra sạch theo từng môi trường.
Khám phá các tính năng liên quan
Tìm hiểu thêm những cách PostTTS giúp bạn thu hút khán giả
Xây dựng với API
REST API đầy đủ cho tích hợp tuỳ chỉnh.
Tìm hiểu thêmNói mọi ngôn ngữ
Tiếp cận khán giả toàn cầu với giọng đọc ở 11 ngôn ngữ: Trung, Anh, Nhật, Hàn, Đức, Pháp, Nga, Việt, Bồ Đào Nha, Tây Ban Nha và Ý.
Tìm hiểu thêmLuôn được cập nhật
PostTTS tự phát hiện thay đổi nội dung và tạo lại âm thanh để mọi thứ luôn đồng bộ.
Tìm hiểu thêmBắt đầu dùng PostTTS API
Tạo tài khoản miễn phí, lấy API key và gửi request đầu tiên trong chưa đến một phút.
Tạo tài khoản miễn phíGói miễn phí gồm 10 bài viết/tháng · Không cần thẻ tín dụng