PostTTS logo PostTTS

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

1

Đă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.

2

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.

3

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.

Quản lý API key của bạn

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_idid (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.

Nghe đọc bài
0:00

Đồ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"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_urlnull. 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

Ý 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 tv1 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.

Bắ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