PostTTS REST API
Generate audio programmatically from any backend. Simple, language-agnostic HTTP API with Bearer-token authentication.
JSON over HTTPS · Bearer auth · Works with any language
Quick Start
Sign up and create an API key
Create a free account and generate an API key from the Integration page in your dashboard. Keys are shown once — copy and store them securely.
POST your post URL or text
Send a POST to /api/v1/posts/convert with your site_id, the post URL (or raw text), and the voice you want.
Receive an audio URL
The response contains an audio URL once conversion completes. For long posts, poll GET /api/v1/posts/:id/audio until status === "ready".
Authentication
All requests authenticate with a Bearer token in the Authorization header.
Bearer token
Send your API key in every request. Keys are prefixed with ptts_ so they're easy to spot in logs and env files.
Authorization: Bearer ptts_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json Keys are shown once at creation and hashed at rest — treat them like passwords. If a key is exposed, revoke it immediately and issue a new one.
Endpoints Reference
All endpoints live under https://posttts.com.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/posts/convert | Start converting a post (text or URL) to audio |
| GET | /api/v1/posts/:id/audio | Fetch audio status and URL for a converted post |
| GET | /api/v1/sites/:site_id/posts | List converted posts for a site |
| GET | /api/v1/voices | List available voices |
| DELETE | /api/v1/posts/:id | Delete a converted post and its audio |
Request Examples
A full POST to /api/v1/posts/convert in four languages.
curl -X POST https://posttts.com/api/v1/posts/convert \
-H "Authorization: Bearer ptts_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"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+ with requests
import os
import requests
response = requests.post(
"https://posttts.com/api/v1/posts/convert",
headers={
"Authorization": f"Bearer {os.environ['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: {$status}");
}
$post = json_decode($response, true);
echo $post['audio_url']; Response Example
A successful conversion returns JSON with the audio URL and metadata.
{
"id": "post_01HX...",
"status": "ready",
"audio_url": "https://cdn.posttts.com/audio/post_01HX.m4a",
"voice": "woman",
"duration_seconds": 184,
"created_at": "2026-04-05T12:00:00Z"
} Preview what clients get back
The audio_url field points at an m4a like this one.
Status Codes & Errors
| Code | Meaning |
|---|---|
| 200 | OK. Request succeeded. |
| 201 | Created. Resource created successfully. |
| 202 | Accepted. Conversion queued; poll for status. |
| 400 | Bad Request. Missing or malformed parameter. |
| 401 | Unauthorized. Invalid or missing API key. |
| 403 | Forbidden. Key lacks permission for this resource. |
| 404 | Not Found. Resource does not exist. |
| 409 | Conflict. Post already converted or in progress. |
| 429 | Too Many Requests. Rate limit exceeded; back off. |
| 500 | Internal Server Error. Something went wrong on our side. |
Errors always come back as JSON with an error object containing a machine-readable code, human-readable message, and the HTTP status.
{
"error": {
"code": "invalid_api_key",
"message": "The provided API key is invalid or has been revoked.",
"status": 401
}
} Rate Limits
Every plan has a per-minute request quota. Stay under it and you'll never see a 429.
Free plan
60 req/min
Plenty for most personal projects.
Paid plans
Higher per tier
Scales with your subscription.
Response headers
Every response carries your current quota state so you can self-throttle:
X-RateLimit-LimitYour quota for the current window.X-RateLimit-RemainingRequests left in the current window.X-RateLimit-ResetUnix timestamp when the window resets.
On 429 responses, back off exponentially (250ms → 500ms → 1s → 2s …) and retry. Don't hammer the API.
Frequently Asked Questions
Any language that can make an HTTPS request. We show cURL, Node.js, Python, and PHP above, but Ruby, Go, Rust, Java, C#, Elixir, and anything else with an HTTP client all work identically — it's plain JSON over HTTPS with a Bearer header.
Head to the Integration page in your dashboard. You can generate a new key, and revoke old ones with one click. Revoked keys stop working immediately. We recommend rotating keys on a schedule and any time a key might have been exposed.
Short posts (under ~1,000 words) finish in a few seconds. Longer posts can take up to a minute or two. For anything beyond a short post, treat the API as asynchronous: accept the initial response, then poll GET /api/v1/posts/:id/audio until status === "ready".
Both. Short posts may return a ready audio_url in the initial POST response. Longer posts return status: "processing" and a 202 Accepted — in that case poll the audio endpoint until it flips to "ready". The response shape is identical either way.
You'll get a 429 Too Many Requests response with a Retry-After header and the usual X-RateLimit-* headers. Back off exponentially and retry. Consistent 429s mean it's time to upgrade your plan.
You can, but we recommend creating separate keys for development, staging, and production. That way if a dev key leaks you can revoke it without touching production traffic, and audit logs stay clean per environment.
Start using the PostTTS API
Create a free account, grab your API key, and make your first request in under a minute.
Create Free AccountFree plan includes 10 posts/month · No credit card required