REST API

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

1

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.

2

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.

3

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.

Manage your API keys

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.

Listen to the reading
0:00

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 Account

Free plan includes 10 posts/month · No credit card required