TTS API async jobs and webhooks for long scripts — Audra

POST /v2/speech is synchronous: hold the connection, wait minutes, get audio. That works to 50,000 characters, but a long render holding an HTTP connection open for an hour is a timeout waiting to happen — yours, your proxy's, or your serverless platform's. POST /v2/speech/jobs is the long-form answer: queue the text, get a job id, collect the audio later.

This post is the jobs API reference: request shape, status lifecycle, polling versus webhooks, the three queue modes, and the failure modes to handle.

Creating a job

The job body is the same shape as sync speech — model, text, voice, format, delivery_profile — plus job-specific fields: webhook_url and mode. text accepts up to 500,000 characters; anything past that must be split by you, at chapter or section boundaries.

curl
curl https://audratalks.com/v2/speech/jobs \
  -H "Authorization: Bearer sk-audra-..." \
  -H "Content-Type: application/json" \
  -d '{"model":"audra-essential","voice":"june","text":"<your long-form text up to 500k chars>","format":"mp3","mode":"standard","webhook_url":"https://yourapp.example/hooks/tts"}' 
Python
import requests

resp = requests.post(
    "https://audratalks.com/v2/speech/jobs",
    headers={"Authorization": "Bearer sk-audra-..."},
    json={
        "model": "audra-essential",
        "voice": "june",
        "text": long_text,
        "format": "mp3",
        "mode": "standard",
        "webhook_url": "https://yourapp.example/hooks/tts",
    },
    timeout=60,
)
resp.raise_for_status()
job = resp.json()  # {"job_id": "job_...", ...}
JavaScript
const res = await fetch("https://audratalks.com/v2/speech/jobs", {
  method: "POST",
  headers: { Authorization: "Bearer sk-audra-...", "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "audra-essential",
    voice: "june",
    text: longText,
    format: "mp3",
    mode: "standard",
    webhook_url: "https://yourapp.example/hooks/tts",
  }),
});
if (!res.ok) throw new Error(await res.text());
const job = await res.json(); // { job_id: "job_...", ... }

Status lifecycle

A job moves through queued → processing → completed (or failed). Poll GET /v2/speech/jobs/{id} for the current status; the response includes job_id, status, chars, input_chars, model, mode, voice, format, error (null unless failed), created_at, and completed_at. When status is completed, download the audio from GET /v2/speech/jobs/{id}/audio.

Webhooks versus polling

Pass webhook_url at creation and Audra POSTs { job_id, status, chars } to your endpoint on completion or failure — no polling loop, no sleep-30-seconds scripts. Webhooks win for any pipeline enqueueing more than a couple of jobs. Polling is fine for one-off scripts and for backfilling a webhook your endpoint missed. Verify your handler is idempotent: delivery retries can fire more than once for the same job.

Queue modes: standard, fast, economy

mode is jobs-only. standard (1.0x credits) is the default. fast buys queue priority at 1.25x. economy defers rendering to the next scheduled sweep (roughly every 12 hours, or sooner if a worker is already warm) at 0.75x credits — 25% off for batch work with no deadline. Sending mode economy on the synchronous /v2/speech endpoint returns 400; economy exists only on jobs.

Failure modes and fixes

When to stay synchronous

Under ~8,000 characters with a patient client, sync /v2/speech is simpler: one request, audio bytes in the response, X-QA-Passed header to check. Jobs earn their complexity when renders run long, when you batch many files, or when your runtime caps request duration.

The audiobook pipeline built on jobs: the audiobook chapters post. Billing behavior of failed renders: the QA-gated billing post. Full parameter reference: /docs.