> ## Documentation Index
> Fetch the complete documentation index at: https://docs.audiopod.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Audiobook

> Turn a manuscript into a finished, ACX-compliant audiobook through a fully automated API: create a project, upload a PDF or EPUB, narrate it, and download a retail-ready package — with completion webhooks for hands-off, backlog-scale production.

## Overview

The Audiobook API exposes the **complete production pipeline** end to end, so you can drop AI audiobook generation directly into your own platform or control panel: a file goes in, a finished, ACX-compliant package comes back, with no human in the loop. It is the same API surface our own Audiobook Studio runs on — nothing here is a private endpoint.

```
create project → add manuscript → parse into chapters → narrate → export ACX package → download
                                                              └──── completion webhook ────┘
```

Every endpoint is API-key authenticated and asynchronous: submit a job, get an ID back immediately, and receive a signed webhook when it finishes — so your control panel never blocks on a long render.

### Key features

* **Inputs**: upload PDF, EPUB, DOCX, or TXT; paste raw text; or generate a book from a prompt. Chapters are detected automatically.
* **Voices**: 100+ languages with standard narration voices; custom voice cloning across 20+ languages.
* **Auto Direction**: a per-paragraph performance note and emotion written across the whole book, anchored to a narration brief so the tone holds from first chapter to last — dialogue directed as dialogue, instead of a flat read. Free on every plan.
* **ACX-compliant export**: per-chapter MP3 at **192 kbps CBR / 44.1 kHz**, a retail sample, optional narrated opening/closing credits, a cover slot, and a `metadata.json` manifest — packaged as a single ZIP via a presigned URL.
* **Automatic compliance check**: file format, audio levels, silence, and per-chapter length are validated on export; a non-compliant book is rejected with the exact reason rather than shipped silently.
* **Asynchronous + webhooks**: `job.completed` / `job.failed` / `job.cancelled` callbacks for backlog-scale automation.
* **White-label output**: delivered files carry the publisher's own title/author/narrator metadata — nothing points back at AudioPod.
* **Prepaid usage pricing**: $0.04 per minute of output (~$24 for a 10-hour book). See [API Wallet](/account/api-wallet).

## Authentication

All endpoints require authentication. Use an API key (recommended for server-to-server integrations):

* **API Key (Recommended)**: `X-API-Key: your_api_key` header — [create one in the dashboard](https://www.audiopod.ai/dashboard/account/api-keys).
* **JWT Token**: `Authorization: Bearer your_jwt_token` (session-based auth).

## Getting the IDs

Almost every call below is addressed by an ID, and how you obtain them is the first thing to get right.

<Info>
  **Every audiobook identifier is a UUID string**, and in every response body the field is called **`id`** — not `project_id` or `chapter_id`. Those names appear only as *path parameters* (`/projects/{project_id}/chapters/{chapter_id}`) and as *back-references* on child objects (a chapter carries `project_id`; a paragraph carries both `chapter_id` and `project_id`). So you read `id` from the object you just created, and you *write* it into the next request's path.
</Info>

<Warning>
  `GET /audiobook/projects/{project_id}` does **not** return chapters by default — the project body carries `total_chapters` (a count) and `chapters: null`. To get the chapter objects (and therefore their `id`s) you must either pass **`?include_chapters=true`** on the project read, or call `GET /audiobook/projects/{project_id}/chapters` separately.
</Warning>

The full call chain, with the ID each step produces:

| # | Call                                                                                                                                                                         | Gives you                                                                                 |
| - | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| 1 | `POST /audiobook/projects`                                                                                                                                                   | `id` → your **project\_id**                                                               |
| 2 | `POST /audiobook/projects/{project_id}/manuscript/upload`                                                                                                                    | `file_key`                                                                                |
| 3 | `POST /audiobook/projects/{project_id}/manuscript/parse`                                                                                                                     | `job_id` (parsing runs in the background)                                                 |
| 4 | `GET /audiobook/projects/{project_id}?include_chapters=true`                                                                                                                 | poll until parsing finishes — see [Poll for parse completion](#poll-for-parse-completion) |
| 5 | `GET /audiobook/projects/{project_id}/chapters` (or the `chapters` array from step 4)                                                                                        | each chapter's `id` → **chapter\_id**                                                     |
| 6 | `GET /audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs`                                                                                                      | each paragraph's `id` → **paragraph\_id**                                                 |
| 7 | `POST /audiobook/projects/{project_id}/narration/batch` (whole book / chapters) or `POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` (one paragraph) | a narration job                                                                           |

Chapters are the unit you *organise* by; **paragraphs are the unit that is actually narrated, billed, and re-recorded**. If you only ever need whole-book narration you can stop at step 5 — `POST .../narration/batch` with no `chapter_ids` or `paragraph_ids` narrates the entire project.

## API Endpoints Quick Reference

| Operation                                                        | Method         | Endpoint                                                                                    |
| ---------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------- |
| Create project                                                   | POST           | `/audiobook/projects`                                                                       |
| List projects (`page`, `per_page`)                               | GET            | `/audiobook/projects`                                                                       |
| Get project (`?include_chapters=true`)                           | GET            | `/audiobook/projects/{project_id}`                                                          |
| Update / delete project                                          | PUT·DELETE     | `/audiobook/projects/{project_id}`                                                          |
| Upload manuscript                                                | POST           | `/audiobook/projects/{project_id}/manuscript/upload`                                        |
| Parse manuscript                                                 | POST           | `/audiobook/projects/{project_id}/manuscript/parse`                                         |
| Append another manuscript file                                   | POST           | `/audiobook/projects/{project_id}/manuscript/append`                                        |
| Paste text                                                       | POST           | `/audiobook/projects/{project_id}/manuscript/paste`                                         |
| Generate book                                                    | POST           | `/audiobook/projects/{project_id}/manuscript/generate`                                      |
| List chapters (`page`, `per_page`)                               | GET            | `/audiobook/projects/{project_id}/chapters`                                                 |
| Get / update / delete a chapter                                  | GET·PUT·DELETE | `/audiobook/projects/{project_id}/chapters/{chapter_id}`                                    |
| List paragraphs in a chapter                                     | GET            | `/audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs`                         |
| Get / update a paragraph                                         | GET·PUT        | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}`                                |
| **Auto Direction — direct the book**                             | POST           | `/audiobook/projects/{project_id}/direction/auto`                                           |
| Latest direction run                                             | GET            | `/audiobook/projects/{project_id}/direction/auto/latest`                                    |
| Direction run status                                             | GET            | `/audiobook/projects/{project_id}/direction/auto/{job_id}/status`                           |
| **Pronunciation dictionary — list entries**                      | GET            | `/audiobook/projects/{project_id}/pronunciations`                                           |
| Add a pronunciation entry                                        | POST           | `/audiobook/projects/{project_id}/pronunciations`                                           |
| Update a pronunciation entry                                     | PUT            | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}`                        |
| Delete a pronunciation entry                                     | DELETE         | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}`                        |
| List voices                                                      | GET            | `/audiobook/voices/available`                                                               |
| List languages                                                   | GET            | `/audiobook/voices/languages`                                                               |
| Cost estimate                                                    | GET            | `/audiobook/projects/{project_id}/cost-estimate`                                            |
| **Narrate (batch — the billed path)**                            | POST           | `/audiobook/projects/{project_id}/narration/batch`                                          |
| Narrate a single paragraph                                       | POST           | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate`                        |
| Narration progress                                               | GET            | `/audiobook/projects/{project_id}/narration/progress`                                       |
| Cancel narration                                                 | POST           | `/audiobook/projects/{project_id}/narration/cancel`                                         |
| Re-render one chapter                                            | POST           | `/audiobook/projects/{project_id}/narration/batch` with `chapter_ids` + `include_completed` |
| Retry a chapter's failed paragraphs                              | POST           | `/audiobook/projects/{project_id}/chapters/{chapter_id}/retry-failed-paragraphs`            |
| Production settings — opening/closing announcement, music, cover | GET·PUT        | `/audiobook/projects/{project_id}/production-settings`                                      |
| Upload media (cover/music)                                       | POST           | `/audiobook/projects/{project_id}/media/upload`                                             |
| Start ACX export                                                 | POST           | `/audiobook/projects/{project_id}/export`                                                   |
| Export status                                                    | GET            | `/audiobook/projects/{project_id}/export/{job_id}/status`                                   |
| Download package                                                 | GET            | `/audiobook/projects/{project_id}/export/{job_id}/download`                                 |
| Dashboard stats                                                  | GET            | `/audiobook/dashboard`                                                                      |

<Warning>
  `POST /audiobook/projects/{project_id}/narration/start` is **deprecated and removed** — it now returns **`410 Gone`**. Use `POST /audiobook/projects/{project_id}/narration/batch`: with no `chapter_ids` and no `paragraph_ids` it narrates the whole project, which is exactly what `narration/start` used to do.

  `POST /audiobook/projects/{project_id}/chapters/{chapter_id}/regenerate` is **deprecated and removed** and also returns **`410 Gone`**. Use `POST /audiobook/projects/{project_id}/narration/batch` with `{"chapter_ids": ["<chapter-uuid>"], "include_completed": true}` — `include_completed` is required, because a chapter you are RE-rendering already has audio and would otherwise be skipped entirely.
</Warning>

## Quick Start (cURL)

```bash theme={null}
export AUDIOPOD_API_KEY="ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AP="https://api.audiopod.ai/api/v1"

# 1. Create a project
PID=$(curl -s -X POST "$AP/audiobook/projects" -H "X-API-Key: $AUDIOPOD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"The Lighthouse Keeper","author":"A. P. Tester"}' | jq -r .id)

# 2. Upload a manuscript (PDF / EPUB / DOCX / TXT)
FILE_KEY=$(curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/upload" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file=@book.epub" | jq -r .file_key)

# 3. Parse into chapters (async)
curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file_key=$FILE_KEY" -F "filename=book.epub"

# 3b. Poll the PROJECT until parsing finishes — status tells you which way it went
while :; do
  P=$(curl -s "$AP/audiobook/projects/$PID?include_chapters=true" -H "X-API-Key: $AUDIOPOD_API_KEY")
  S=$(echo "$P" | jq -r .status)
  [ "$S" = "failed" ] && { echo "parse failed: $(echo "$P" | jq -r .error_message)"; exit 1; }
  [ "$(echo "$P" | jq -r .parsing_completed_at)" != "null" ] && break
  sleep 5
done
echo "$P" | jq '.chapters[] | {id, chapter_number, title}'

# 4. Narrate the whole project with a voice (omit chapter_ids/paragraph_ids = everything)
curl -s -X POST "$AP/audiobook/projects/$PID/narration/batch" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d '{"voice_id":387}'

# 5. Export an ACX package, then download it
JOB=$(curl -s -X POST "$AP/audiobook/projects/$PID/export" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .job_id)
curl -s "$AP/audiobook/projects/$PID/export/$JOB/download" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "download_url": "https://media.audiopod.ai/...zip?...", "file_size_bytes": ..., "chapter_count": ... }
```

## 1. Create a project

A project is the container for a single audiobook. Create it first, then attach a manuscript.

<ParamField body="title" type="string" required>
  Audiobook title.
</ParamField>

<ParamField body="author" type="string">
  Author name (written into the package metadata).
</ParamField>

<ParamField body="description" type="string">
  Project description.
</ParamField>

<ParamField body="isbn" type="string">
  ISBN, if available.
</ParamField>

<ParamField body="genre" type="string">
  Book genre.
</ParamField>

<ParamField body="selected_voice_id" type="integer">
  Default narration voice for the project (can also be set per narration request).
</ParamField>

<ParamField body="narration_speed" type="number" default="1.0">
  Speech speed multiplier.
</ParamField>

<ParamField body="narration_style" type="string" default="narrative">
  Narration style.
</ParamField>

<ParamField body="export_format" type="string" default="acx">
  Export format. `acx` produces an Audible/ACX-ready package.
</ParamField>

<ParamField body="target_platform" type="string" default="audible">
  Target distribution platform.
</ParamField>

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "$AP/audiobook/projects" -H "X-API-Key: $AUDIOPOD_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"title":"The Lighthouse Keeper","author":"A. P. Tester","genre":"Fiction"}'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests
    AP, H = "https://api.audiopod.ai/api/v1", {"X-API-Key": "ap_..."}
    project = requests.post(f"{AP}/audiobook/projects", headers=H, json={
        "title": "The Lighthouse Keeper", "author": "A. P. Tester", "genre": "Fiction",
    }).json()
    pid = project["id"]
    ```
  </Tab>
</Tabs>

**Response** (`AudiobookProjectResponse`):

<ResponseField name="id" type="string">Project UUID — **this is your `project_id`** for every subsequent call.</ResponseField>
<ResponseField name="status" type="string">Lifecycle state (see [Status values](#status-values)).</ResponseField>
<ResponseField name="total_chapters" type="integer">Number of chapters once parsed. This is a *count* — the chapter objects are not in this response unless you ask for them.</ResponseField>
<ResponseField name="estimated_duration_minutes" type="number">Estimated finished runtime.</ResponseField>
<ResponseField name="manuscript_filename" type="string">Uploaded filename, once attached.</ResponseField>
<ResponseField name="manuscript_format" type="string">`pdf` · `epub` · `docx` · `txt`.</ResponseField>
<ResponseField name="manuscript_word_count" type="integer">Total words after parsing.</ResponseField>
<ResponseField name="parsing_completed_at" type="string">ISO timestamp set when a manuscript parse finished successfully; `null` until then.</ResponseField>
<ResponseField name="narration_started_at" type="string">ISO timestamp of the first narration job.</ResponseField>
<ResponseField name="narration_completed_at" type="string">ISO timestamp when narration finished.</ResponseField>
<ResponseField name="production_completed_at" type="string">ISO timestamp when production/export finished.</ResponseField>
<ResponseField name="error_message" type="string">Sanitized failure reason when a stage failed; `null` otherwise.</ResponseField>
<ResponseField name="selected_voice_id" type="integer">Project default narration voice.</ResponseField>
<ResponseField name="narration_speed" type="number">Speed multiplier (0.5–2.0).</ResponseField>
<ResponseField name="narration_style" type="string">Delivery style.</ResponseField>
<ResponseField name="export_format" type="string">Export format (`acx`).</ResponseField>
<ResponseField name="target_platform" type="string">Target distribution platform.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>
<ResponseField name="updated_at" type="string">Last-modified timestamp.</ResponseField>

### Read one project

`GET /audiobook/projects/{project_id}` returns the same body, plus a `chapters` field:

<ParamField query="include_chapters" type="boolean" default="false">
  When `true`, the response carries the project's full `chapters` array inline, saving a second round-trip. When omitted or `false`, `chapters` is `null` — the chapter objects (and their `id`s) are simply not there.
</ParamField>

```bash theme={null}
curl -s "$AP/audiobook/projects/$PID?include_chapters=true" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "id": "...", "status": "ready_for_narration", "total_chapters": 12,
#      "parsing_completed_at": "2026-07-23T09:14:02", "error_message": null,
#      "chapters": [ { "id": "...", "chapter_number": 1, "title": "...", ... } ] }
```

### List projects

`GET /audiobook/projects` is paginated with **`page`** and **`per_page`** (not `skip`/`limit`):

<ParamField query="page" type="integer" default="1">1-based page number (minimum `1`).</ParamField>
<ParamField query="per_page" type="integer" default="20">Projects per page, `1`–`100`.</ParamField>
<ParamField query="status_filter" type="string">Return only projects in this [status](#status-values).</ParamField>
<ParamField query="search" type="string">Case-insensitive substring match on title or author.</ParamField>

```bash theme={null}
curl -s "$AP/audiobook/projects?page=2&per_page=50" -H "X-API-Key: $AUDIOPOD_API_KEY"
```

<ResponseField name="projects" type="array">The page of projects (each an `AudiobookProjectResponse`).</ResponseField>
<ResponseField name="total" type="integer">Total projects matching the filters, across all pages.</ResponseField>
<ResponseField name="page" type="integer">Echo of the requested page.</ResponseField>
<ResponseField name="per_page" type="integer">Echo of the requested page size.</ResponseField>
<ResponseField name="has_next" type="boolean">Whether another page follows.</ResponseField>
<ResponseField name="has_prev" type="boolean">Whether a previous page exists.</ResponseField>

<Warning>
  Unknown query parameters are **ignored, not rejected**. Sending `?skip=0&limit=20` returns a `200` with the *first* page every time — so a paging loop built on `skip`/`limit` never advances and silently re-reads page 1 forever. Use `page` / `per_page`, and drive your loop off `has_next`.
</Warning>

## 2. Add the manuscript

Three ways to get text into a project — pick one.

### Upload a file

`POST /audiobook/projects/{project_id}/manuscript/upload` — multipart form field `file` (PDF, EPUB, DOCX, or TXT). Returns a `file_key` to hand to the parser.

<ResponseField name="file_key" type="string">Storage key of the uploaded manuscript — pass this to the parser.</ResponseField>
<ResponseField name="upload_url" type="string">Reference URL for the stored file.</ResponseField>
<ResponseField name="max_file_size" type="integer">Maximum accepted size in bytes.</ResponseField>

```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/manuscript/upload" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file=@book.epub"
```

### Parse the uploaded file into chapters

`POST /audiobook/projects/{project_id}/manuscript/parse` — pass `file_key` (from the upload response) and `filename`. The endpoint accepts a **JSON body** or **form fields** (`multipart/form-data` or `application/x-www-form-urlencoded`) — use whichever your HTTP client makes easiest. Parsing is asynchronous and returns a `job_id` immediately — see [Poll for parse completion](#poll-for-parse-completion) for how to wait on it.

```bash theme={null}
# JSON body
curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d "{\"file_key\": \"$FILE_KEY\", \"filename\": \"book.epub\"}"

# ...or form fields (equivalent)
curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -F "file_key=$FILE_KEY" -F "filename=book.epub"
# => { "job_id": 99, "estimated_completion_time": "under 2 minutes" }
```

<ParamField body="file_key" type="string" required>Storage key returned by the upload response.</ParamField>
<ParamField body="filename" type="string" required>Original filename, including the extension.</ParamField>
<ParamField body="force" type="boolean" default="false">Re-parse a project that already has chapters. **Destructive** — existing chapters and paragraphs are replaced.</ParamField>
<ParamField body="auto_direction" type="boolean">Run [Auto Direction](#auto-direction) as soon as parsing finishes, so the book narrates expressively without a second call. **Defaults to `false` for API-key callers** — see below.</ParamField>

<Note>
  **`auto_direction` defaults to off for API keys, on purpose.** Auto Direction rewrites the `direction` on every paragraph, which changes how your next narration sounds. We are not going to change your integration's output without you asking for it, so an API-key parse leaves directions alone unless you pass `auto_direction=true`. (In the web studio the default is the other way round — a book should already sound directed when the author opens it.)

  You can also direct a book at any time after parsing with [`POST .../direction/auto`](#auto-direction) — nothing about it is tied to parse time.
</Note>

```bash theme={null}
# Parse AND direct in one call
curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d "{\"file_key\": \"$FILE_KEY\", \"filename\": \"book.epub\", \"auto_direction\": true}"
```

### Poll for parse completion

<Warning>
  There is **no parse-status endpoint**, and polling `GET .../chapters` "until chapters appear" is not a safe wait — a *failed* parse produces no chapters, so that loop never terminates. Poll the **project** instead: it is the object that carries the parse outcome.
</Warning>

Poll `GET /audiobook/projects/{project_id}?include_chapters=true` — one call returns both the parse state and, on success, the chapters — and branch on these three fields:

<ResponseField name="status" type="string">
  `parsing` while the job is running. On success it becomes `ready_for_narration`. On a failed **first** parse it becomes `failed`.
</ResponseField>

<ResponseField name="parsing_completed_at" type="string | null">
  `null` until a parse has completed successfully; an ISO timestamp afterwards. **This is the success signal.**
</ResponseField>

<ResponseField name="error_message" type="string | null">
  The sanitized failure reason, set when the parse fails (e.g. an unreadable or over-length manuscript). Read it to report *why* to your user.
</ResponseField>

Decision table — evaluate in this order:

| Condition                      | Meaning               | Do                                                              |
| ------------------------------ | --------------------- | --------------------------------------------------------------- |
| `status == "failed"`           | **Terminal failure.** | Stop. Surface `error_message`; fix or re-upload the manuscript. |
| `parsing_completed_at != null` | **Success.**          | Stop. Read `chapters` (or call the chapters endpoint).          |
| `status == "parsing"`          | Still working.        | Sleep and poll again.                                           |
| Neither, after your timeout    | Treat as failed.      | Give up after a bounded number of attempts.                     |

<Note>
  Check `status` **before** `error_message`. `error_message` is not cleared when you retry a parse, so a leftover value from an earlier failure can persist on a project that has since parsed fine. `status` (plus `parsing_completed_at`) is the authoritative state; `error_message` is the *reason* to display once `status` says `failed`.

  One nuance for `manuscript/append`: if an append-parse fails on a project that **already** has chapters, the project is returned to `ready_for_narration` rather than `failed`, so the existing book is not destroyed. In that case compare `total_chapters` before and after to detect that nothing was added.
</Note>

```python theme={null}
import time, requests

AP, H = "https://api.audiopod.ai/api/v1", {"X-API-Key": "ap_..."}

def wait_for_parse(pid: str, timeout_s: int = 900, interval_s: int = 5) -> list[dict]:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        p = requests.get(f"{AP}/audiobook/projects/{pid}",
                         params={"include_chapters": "true"}, headers=H).json()
        if p["status"] == "failed":                       # terminal failure
            raise RuntimeError(p.get("error_message") or "Manuscript parsing failed")
        if p.get("parsing_completed_at"):                 # terminal success
            return p["chapters"] or []
        time.sleep(interval_s)                            # still parsing
    raise TimeoutError("Manuscript parsing did not finish in time")

chapters = wait_for_parse(pid)
chapter_ids = [c["id"] for c in chapters]
```

Parses typically finish in under two minutes for EPUB/DOCX/TXT and in 2–10 minutes for large or scanned PDFs; the `estimated_completion_time` string in the parse response reflects the format you submitted. A 15-minute poll ceiling is a safe default.

### Paste text directly

`POST /audiobook/projects/{project_id}/manuscript/paste` — JSON. Best for content you already have as text.

<ParamField body="text" type="string" required>Manuscript or chapter text.</ParamField>
<ParamField body="source_type" type="string" default="paste">Source label.</ParamField>

### Generate a book from a prompt

`POST /audiobook/projects/{project_id}/manuscript/generate` — JSON. Drafts an original manuscript, then chapters it.

<ParamField body="prompt" type="string" required>What the book should be about.</ParamField>
<ParamField body="title" type="string">Optional working title.</ParamField>
<ParamField body="audience" type="string">Intended audience.</ParamField>
<ParamField body="tone" type="string" default="warm">Writing tone.</ParamField>
<ParamField body="chapter_count" type="integer" default="3">Number of chapters.</ParamField>
<ParamField body="words_per_chapter" type="integer" default="600">Target words per chapter.</ParamField>

The `paste` and `generate` endpoints return a `ManuscriptIntakeResponse` with `total_chapters`, `total_words`, and `estimated_duration_minutes`.

<Warning>
  `manuscript/parse`, `manuscript/paste`, and `manuscript/generate` **replace the project's entire chapter/paragraph structure** — any existing narration is discarded. Parse once per manuscript; re-parse only if the manuscript file itself changed.

  While narration, another parse, mixing, or an export is already running for the project, these endpoints return `409 Conflict` — poll `GET .../narration/progress` and retry once it finishes. If you don't want to wait for a narration run to finish, stop it first with [`POST .../narration/cancel`](#cancel-a-narration-run); already-narrated paragraphs are kept.

  If the project already has completed narration, these endpoints instead return `409` with a structured body: `detail.code = "completed_narration_exists"` and `detail.completed_paragraphs = N`. To proceed anyway, pass `force=true` (accepted as a JSON field, form field, or query parameter). **Do not hard-code `force=true` in your integration** — treat this `409` as a stop-and-confirm; forcing permanently deletes the `N` narrated paragraphs.

  `manuscript/append` is the exception: it's additive (appends chapters after the existing ones) and never deletes narration.
</Warning>

## 3. Chapters

List chapters with `GET /audiobook/projects/{project_id}/chapters`, or fetch one with `GET .../chapters/{chapter_id}`.

<ParamField query="page" type="integer" default="1">1-based page number (minimum `1`).</ParamField>
<ParamField query="per_page" type="integer" default="50">Chapters per page, `1`–`500`.</ParamField>

The response is the same paginated envelope as the project list — `chapters`, `total`, `page`, `per_page`, `has_next`, `has_prev` — with chapters ordered by `chapter_number`. As with projects, `skip`/`limit` are **not** recognised and are silently ignored.

```bash theme={null}
curl -s "$AP/audiobook/projects/$PID/chapters?page=1&per_page=100" -H "X-API-Key: $AUDIOPOD_API_KEY"
```

**Chapter** (`AudiobookChapterResponse`):

<ResponseField name="id" type="string">Chapter UUID — **this is your `chapter_id`**.</ResponseField>
<ResponseField name="project_id" type="string">Back-reference to the owning project.</ResponseField>
<ResponseField name="chapter_number" type="integer">Order in the book.</ResponseField>
<ResponseField name="title" type="string">Chapter title.</ResponseField>
<ResponseField name="content" type="string">Chapter text.</ResponseField>
<ResponseField name="word_count" type="integer">Words in the chapter.</ResponseField>
<ResponseField name="character_count" type="integer">Characters in the chapter.</ResponseField>
<ResponseField name="duration_seconds" type="number">Narrated length, once generated.</ResponseField>
<ResponseField name="status" type="string">`pending` · `queued` · `narrating` · `completed` · `failed`.</ResponseField>
<ResponseField name="audio_file_path" type="string">Stored audio key, once narrated.</ResponseField>
<ResponseField name="error_message" type="string">Reason a chapter failed (e.g. content too short for narration).</ResponseField>
<ResponseField name="skip_narration" type="boolean">If true, the chapter is excluded from narration and export.</ResponseField>
<ResponseField name="voice_id" type="integer">Per-chapter voice override.</ResponseField>
<ResponseField name="narration_notes" type="string">Free-text narration instructions stored on the chapter.</ResponseField>
<ResponseField name="estimated_duration_minutes" type="number">Estimated narrated length before rendering.</ResponseField>
<ResponseField name="retry_count" type="integer">Number of narration retries so far.</ResponseField>

You can edit a chapter (title, content, `skip_narration`, per-chapter `voice_id`, narration notes) with `PUT .../chapters/{chapter_id}` before narrating.

## 3b. Paragraphs

Chapters are containers; **paragraphs are the atomic unit of narration**. Each paragraph is narrated, billed, re-recorded, and version-tracked (as "takes") independently, so paragraph-level access is what you want for fine-grained control — fixing one mispronounced sentence without re-rendering a 40-minute chapter.

### List a chapter's paragraphs

`GET /audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs` — returned in `position` order.

<ParamField query="page" type="integer">1-based page number. **Optional.**</ParamField>
<ParamField query="per_page" type="integer">Paragraphs per page, `1`–`200`. **Optional**; defaults to 50 when only `page` is given.</ParamField>

<Note>
  Pagination here is **opt-in**: omit *both* `page` and `per_page` and you get every paragraph in the chapter in a single response. Supply either one and paging switches on. `total` is always the chapter's full paragraph count, never the length of the page you received.
</Note>

**Response** (`PaginatedParagraphsResponse`):

<ResponseField name="paragraphs" type="array">The paragraphs, ordered by `position`.</ResponseField>
<ResponseField name="total" type="integer">Total paragraphs in the chapter.</ResponseField>
<ResponseField name="page" type="integer | null">Echo of the requested page; `null` in unpaginated mode.</ResponseField>
<ResponseField name="per_page" type="integer | null">Echo of the requested page size; `null` in unpaginated mode.</ResponseField>
<ResponseField name="has_next" type="boolean">Whether another page follows (`false` in unpaginated mode).</ResponseField>
<ResponseField name="has_prev" type="boolean">Whether a previous page exists (`false` in unpaginated mode).</ResponseField>

**Paragraph** (`AudiobookParagraphResponse`):

<ResponseField name="id" type="string">Paragraph UUID — **this is your `paragraph_id`**.</ResponseField>
<ResponseField name="chapter_id" type="string">Back-reference to the owning chapter.</ResponseField>
<ResponseField name="project_id" type="string">Back-reference to the owning project.</ResponseField>
<ResponseField name="position" type="integer">Order within the chapter (the sort key).</ResponseField>
<ResponseField name="text" type="string">The text that will be narrated.</ResponseField>
<ResponseField name="word_count" type="integer">Words in the paragraph.</ResponseField>
<ResponseField name="character_count" type="integer">Characters in the paragraph (narration is metered per character).</ResponseField>
<ResponseField name="voice_id" type="integer">Per-paragraph voice override, if set.</ResponseField>
<ResponseField name="narration_speed" type="number">Per-paragraph speed override.</ResponseField>
<ResponseField name="narration_style" type="string">Per-paragraph delivery style.</ResponseField>
<ResponseField name="emotion" type="string">Emotional direction for this paragraph.</ResponseField>
<ResponseField name="character_name" type="string">Speaking character, when the book is cast.</ResponseField>
<ResponseField name="direction" type="string">Screenplay-style direction applied to this line.</ResponseField>
<ResponseField name="pause_after_ms" type="integer">Silence appended after this paragraph, in milliseconds.</ResponseField>
<ResponseField name="audio_file_path" type="string">Stored audio key, once narrated.</ResponseField>
<ResponseField name="duration_seconds" type="number">Narrated length, once generated.</ResponseField>
<ResponseField name="status" type="string">`pending` · `queued` · `narrating` · `completed` · `failed`.</ResponseField>
<ResponseField name="locked" type="boolean">A locked paragraph is protected: edits and regeneration are refused with `409` until it is unlocked.</ResponseField>
<ResponseField name="skip_narration" type="boolean">If true, the paragraph is excluded from narration and export.</ResponseField>
<ResponseField name="error_message" type="string">Sanitized reason this paragraph failed.</ResponseField>
<ResponseField name="retry_count" type="integer">Narration retries so far.</ResponseField>

```bash theme={null}
# every paragraph in the chapter
curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" | jq '.paragraphs[] | {id, position, status}'

# or a page at a time
curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs?page=1&per_page=50" \
  -H "X-API-Key: $AUDIOPOD_API_KEY"
```

Read one paragraph with `GET .../projects/{project_id}/paragraphs/{paragraph_id}` (handy for status polling) and edit one with `PUT .../projects/{project_id}/paragraphs/{paragraph_id}`.

### Narrate a single paragraph

`POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` generates — or regenerates — audio for exactly one paragraph and records a new *take*. The body is optional; send `{}` to narrate with the paragraph's existing settings.

<ParamField body="voice_id" type="integer">Voice for this take; falls back to the paragraph's, then the project's, voice.</ParamField>
<ParamField body="text_override" type="string">Narrate this text instead of the stored paragraph text (the stored text is unchanged).</ParamField>
<ParamField body="narration_speed" type="number">Speed multiplier for this take, `0.5`–`2.0`.</ParamField>
<ParamField body="narration_style" type="string">Delivery style for this take.</ParamField>
<ParamField body="emotion" type="string">Emotional direction for this take.</ParamField>
<ParamField body="character_name" type="string">Speaking character (max 120 chars).</ParamField>
<ParamField body="direction" type="string">Performance direction for this take; takes precedence over `narration_style`. The field accepts 500 characters but only the first **110** are performed — see [Auto Direction](#auto-direction) for how to write one, and let it write them for you.</ParamField>

**Response:**

<ResponseField name="task_id" type="string">Background task identifier for this narration.</ResponseField>
<ResponseField name="paragraph_id" type="string">The paragraph being narrated.</ResponseField>
<ResponseField name="status" type="string">`queued` — the paragraph's status is set to `queued` immediately.</ResponseField>
<ResponseField name="billing" type="object">`is_free_regen`, `estimated_credits`, `estimated_chars`, `estimated_seconds`, `take_number_after`.</ResponseField>

```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID/narrate" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d '{"voice_id":387,"emotion":"wistful"}'
# => { "task_id": "...", "paragraph_id": "...", "status": "queued",
#      "billing": { "is_free_regen": true, "estimated_credits": 0, "take_number_after": 2 } }
```

Poll `GET .../projects/{project_id}/paragraphs/{paragraph_id}` until `status` is `completed` (or `failed`, with `error_message`). The first take of a paragraph is billed, the **first regeneration is free**, and subsequent regenerations are billed again — `billing.is_free_regen` tells you which case you are in *before* the work runs. A `409` means the paragraph is locked; unlock it with `PUT .../paragraphs/{paragraph_id}` first.

## 3c. Auto Direction

A narrator that reads every line the same way sounds flat, however good the voice is. **Direction** is what fixes that: a short performance note on each paragraph — how the line is delivered, and with what feeling — that the narrator acts on.

You can write directions yourself, one paragraph at a time (see [Direct a single line](#direct-a-single-line)). Auto Direction writes them for the whole book in one call.

`POST /audiobook/projects/{project_id}/direction/auto`

It runs in two passes. First a **narration brief** for the book as a whole — its reading identity, derived from the metadata and the opening pages. Then a performance note and an emotion per paragraph, each anchored to that brief, so the tone holds across a long book instead of drifting chapter to chapter. Dialogue is directed as dialogue.

<ParamField body="chapter_ids" type="string[]">Direct only these chapters. Omit to direct the whole book.</ParamField>

```bash theme={null}
# Direct the whole book
curl -X POST "$AP/audiobook/projects/$PID/direction/auto" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}'
# => { "job_id": "901", "total_chapters": 23 }

# ...or one chapter
curl -X POST "$AP/audiobook/projects/$PID/direction/auto" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d "{\"chapter_ids\": [\"$CHAPTER_ID\"]}"
```

**Auto Direction is free on every plan**, including the free tier. It is the same class of work as chapter detection, and a book that only sounds good on a paid plan is a worse product.

### Waiting for a direction run

```bash theme={null}
# Status of a specific run...
curl "$AP/audiobook/projects/$PID/direction/auto/901/status" -H "X-API-Key: $AUDIOPOD_API_KEY"
# ...or of the most recent one, when you did not keep the job_id
curl "$AP/audiobook/projects/$PID/direction/auto/latest" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "job_id": "901", "status": "COMPLETED", "progress": 100,
#      "result": { "directed_count": 315, "skipped_user_edited": 4,
#                  "chapters_processed": 23, "total_chapters": 23 } }
```

Poll until `status` is `COMPLETED` or `FAILED`. A `409` on the POST means a run is already in flight for this project.

<Note>
  **Directing does not re-render audio.** It only writes `direction` and `emotion` onto paragraphs that already exist — it never re-splits chapters and never deletes a take. Paragraphs already narrated keep the audio they have until you narrate them again:

  ```bash theme={null}
  curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
    -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
    -d "{\"voice_id\": 389, \"chapter_ids\": [\"$CHAPTER_ID\"], \"include_completed\": true}"
  ```

  Re-narration is billed normally, so try one chapter before committing a whole book.
</Note>

### Your directions win

A direction you wrote by hand is **never overwritten** by a later automatic run. Writing one via `PUT .../paragraphs/{paragraph_id}` marks that row as yours, and every subsequent Auto Direction skips it — the run reports how many it left alone in `result.skipped_user_edited`.

The suggestion it replaced is kept on the row, so reverting to the automatic version costs nothing and needs no new run.

### Direct a single line

`PUT /audiobook/projects/{project_id}/paragraphs/{paragraph_id}`

```bash theme={null}
curl -X PUT "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d '{"direction": "urgent, low, barely audible", "emotion": "tense"}'
```

<ParamField body="direction" type="string">A short stage note the narrator acts on. See the length note below.</ParamField>
<ParamField body="emotion" type="string">One of `neutral`, `warm`, `tense`, `sad`, `excited`, `intimate`, `dramatic`, `authoritative`. `neutral` means "no particular feeling".</ParamField>

<Warning>
  **Write a direction short.** The field accepts up to 500 characters, but only the first **110** shape the performance — a longer note is trimmed at a word boundary and the rest is not voiced. One instruction of roughly a dozen words works best: `"urgent, low, barely audible"`, not a paragraph of stage notes.

  When you send a longer one, the response carries a `warnings` entry telling you exactly how much of it will not be heard.
</Warning>

## 3d. Pronunciation dictionary

Some words need a nudge no voice selection can give you — the wrong stress on a place name, a word from another language read as if it were English. A **pronunciation dictionary** fixes this per project: map a written word (`source`) to what the narrator should actually say (`alias`), and every future narration or regeneration in every chapter of the project picks it up automatically.

The substitution happens only at synthesis time. The manuscript, the paragraph text returned by the API, and everything in the exported package all keep the original spelling — only what gets spoken changes.

### Manage entries

Same `X-API-Key` auth as the rest of this API.

| Operation       | Method | Endpoint                                                             |
| --------------- | ------ | -------------------------------------------------------------------- |
| List entries    | GET    | `/audiobook/projects/{project_id}/pronunciations`                    |
| Add an entry    | POST   | `/audiobook/projects/{project_id}/pronunciations`                    |
| Update an entry | PUT    | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` |
| Delete an entry | DELETE | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` |

<ParamField body="source" type="string" required>The written word or phrase to match, 1–200 characters.</ParamField>
<ParamField body="alias" type="string" required>What the narrator says instead of `source`, minimum 1 character. See [Writing a good alias](#writing-a-good-alias) below.</ParamField>
<ParamField body="case_sensitive" type="boolean" default="false">When `false`, `source` matches regardless of case.</ParamField>

```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/pronunciations" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d '{"source": "Otranto", "alias": "Òtranto"}'
# => 201 { "id": "...", "source": "Otranto", "alias": "Òtranto", "case_sensitive": false }
```

`POST` returns `201` on success and `409` when `source` already has an entry for this project — update the existing one instead of retrying the create:

```bash theme={null}
curl -X PUT "$AP/audiobook/projects/$PID/pronunciations/$PRONUNCIATION_ID" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d '{"alias": "Òtranto", "case_sensitive": true}'

curl -X DELETE "$AP/audiobook/projects/$PID/pronunciations/$PRONUNCIATION_ID" \
  -H "X-API-Key: $AUDIOPOD_API_KEY"
# => 204
```

### Matching semantics

* **Whole-word.** A match's boundaries are non-alphanumeric characters or the start/end of the text, so `source: "rise"` matches the standalone word `rise` and not the `rise` inside `surprise`.
* **Multi-word sources work**, including hyphens, apostrophes, and periods — `"Mr. Smith"` is a valid `source`.
* **Case-insensitive by default.** Set `case_sensitive: true` to require an exact-case match.
* **Longer sources win.** If two entries' sources overlap on the same text, the longer one is applied.

### Writing a good alias

The safe default, and what works on **every** voice: an accented or phonetic respelling of the same word. It doesn't need to be true IPA — anything that nudges the reader to the right sound works:

| Problem                           | `source`  | `alias`                       |
| --------------------------------- | --------- | ----------------------------- |
| Wrong stress on a place name      | `Otranto` | `Òtranto` (or `Oh-TRAHN-toh`) |
| A word read in the wrong language | `rise`    | `rìse`                        |

<Warning>
  **IPA is advanced, and voice-dependent.** `alias` may instead be a single word's IPA transcription wrapped in forward slashes — one word per pair of slashes, no spaces inside: `/ˈɔtranto/`. This is honored only on voices that support expressive delivery (the [`delivery_mode`](#delivery-mode-how-performed-the-read-is) setting). On a voice without that support, the slash-wrapped notation is stripped before synthesis and **the word is silently dropped from the narration** — not mispronounced, missing. Unless you know the voice you're narrating with supports it, use an accented or phonetic respelling instead; it's honored on every voice.

  Pronunciation fixes belong here, in `alias` (or by editing the paragraph text itself) — not in a paragraph's `direction` field. `direction` shapes performance, not pronunciation, and phonetic notation placed there is stripped rather than spoken.
</Warning>

### Applying a fix without re-rendering the whole book

New entries apply automatically to narration going forward, but a paragraph that was already narrated keeps its existing audio until it is re-rendered. `POST .../narration/batch` accepts `paragraph_ids` (in addition to `chapter_ids`), so you can re-render only the affected lines instead of a whole chapter — get the ids from [`GET .../chapters/{chapter_id}/paragraphs`](#list-a-chapters-paragraphs):

```bash theme={null}
# find the paragraphs that mention the word you just fixed
curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" | jq -r '.paragraphs[] | select(.text | test("Otranto")) | .id'

# re-render only those paragraphs
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d "{\"voice_id\": 387, \"paragraph_ids\": [\"$PARA_ID\"], \"include_completed\": true}"
```

<Note>
  Narration billing is per paragraph: the first take is billed, the first regeneration of a paragraph is free, and later regenerations bill again at the normal rate — the same rule as [manual regeneration](#narrate-a-single-paragraph). `GET /audiobook/projects/{project_id}/cost-estimate` already reflects this per paragraph, so you can check what a targeted re-render will actually cost before you run it.
</Note>

## 4. Choose a voice

`GET /audiobook/voices/available` returns a `VoiceSelectionResponse`:

<ResponseField name="available_voices" type="array">All standard voices you can narrate with (each has an integer `id`).</ResponseField>
<ResponseField name="recommended_voices" type="array">A curated shortlist.</ResponseField>
<ResponseField name="user_custom_voices" type="array">Your own cloned voices (see [Voice Management](/api-reference/voice-management)).</ResponseField>

`GET /audiobook/voices/languages` lists every supported narration locale (100+). Before committing, `GET /audiobook/projects/{project_id}/cost-estimate` returns a per-paragraph breakdown with `word_count`, `estimated_seconds`, and `credits_to_charge`.

## 5. Narrate

### Narrate the book — `narration/batch`

`POST /audiobook/projects/{project_id}/narration/batch` is **the** narration endpoint. It queues one job that narrates the selected paragraphs in order. Scope is chosen by what you send:

* **omit both `chapter_ids` and `paragraph_ids`** → narrate the entire project;
* **`chapter_ids`** → narrate only those chapters;
* **`paragraph_ids`** → narrate only those paragraphs.

<ParamField body="voice_id" type="integer">Voice to narrate with (from `voices/available`). Optional — omit to use each paragraph's/chapter's assigned voice, falling back to the project's `selected_voice_id`.</ParamField>
<ParamField body="chapter_ids" type="array">Optional — UUIDs of chapters to narrate.</ParamField>
<ParamField body="paragraph_ids" type="array">Optional — UUIDs of individual paragraphs to narrate.</ParamField>
<ParamField body="include_completed" type="boolean" default="false">Re-narrate paragraphs that already have finished audio.</ParamField>
<ParamField body="include_locked" type="boolean" default="false">Include locked paragraphs.</ParamField>
<ParamField body="include_skipped" type="boolean" default="false">Include paragraphs marked `skip_narration`.</ParamField>
<ParamField body="narration_settings" type="object" default="{}">Per-run narration settings — see [Delivery mode](#delivery-mode-how-performed-the-read-is) below.</ParamField>

#### Delivery mode — how performed the read is

`narration_settings.delivery_mode` is the single biggest lever on how a book *sounds*:

| Value      | Read                                                                                                                           |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `stable`   | One steady narrator from the first paragraph to the last. Least performance range. **This is the default for book narration.** |
| `balanced` | Between the two.                                                                                                               |
| `creative` | The most range and variation — usually what narrative fiction wants.                                                           |

Books default to `stable` because a long narration is where drift is most audible: across several hundred paragraphs, a narrator whose timbre wanders is worse than one that is a little plain. If your title wants a performed read — fiction, dialogue-heavy work, anything theatrical — ask for it:

```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
  -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
  -d "{\"voice_id\": 389, \"chapter_ids\": [\"$CHAPTER_ID\"], \"include_completed\": true,
       \"narration_settings\": {\"delivery_mode\": \"creative\"}}"
```

`delivery_mode` and [Auto Direction](#auto-direction) work on different things and compose well: delivery mode sets how much range the voice has at all, direction says what to do with it line by line. A directed book on `creative` is the most expressive result available.

Also accepted in `narration_settings`: `speed` (0.5–2.0) and `language` (a bare subtag such as `"it"`, overriding the project's language for this run).

**Response** (`BatchNarrationResponse`):

<ResponseField name="job_id" type="integer">The narration job.</ResponseField>
<ResponseField name="total_paragraphs" type="integer">Paragraphs queued for this run.</ResponseField>
<ResponseField name="skipped_paragraphs" type="integer">Paragraphs excluded by the `include_*` rules above (already completed, locked, or skipped).</ResponseField>
<ResponseField name="estimated_completion_time" type="string">Human-readable estimate.</ResponseField>

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    # whole project
    curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
      -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
      -d '{"voice_id":387}'
    # => {"job_id": 1042, "total_paragraphs": 318, "skipped_paragraphs": 0,
    #     "estimated_completion_time": "Queued"}

    # just two chapters
    curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
      -H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
      -d '{"voice_id":387,"chapter_ids":["<chapter-uuid>","<chapter-uuid>"]}'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    # whole project
    requests.post(f"{AP}/audiobook/projects/{pid}/narration/batch",
                  headers=H, json={"voice_id": 387})

    # or a specific chapter
    requests.post(f"{AP}/audiobook/projects/{pid}/narration/batch",
                  headers=H, json={"voice_id": 387, "chapter_ids": [chapter_id]})
    ```
  </Tab>
</Tabs>

Common non-200s: **`402`** insufficient balance (nothing is queued or charged), **`409`** another narration/parse/mix/export is already live for this project, **`503`** the job could not be queued (nothing charged — retry).

### Track progress

`GET /audiobook/projects/{project_id}/narration/progress` returns a `NarrationProgress`:

<ResponseField name="total_chapters" type="integer">Total chapters.</ResponseField>
<ResponseField name="completed_chapters" type="integer">Finished narrations.</ResponseField>
<ResponseField name="failed_chapters" type="integer">Failed narrations.</ResponseField>
<ResponseField name="in_progress_chapters" type="integer">Currently rendering.</ResponseField>
<ResponseField name="chapter_status" type="array">Per-chapter status detail.</ResponseField>

Narration is complete when `completed_chapters + failed_chapters >= total_chapters`. (Or skip polling and wait for the [completion webhook](#completion-webhooks).)

### Paragraph-level & regeneration

For a single line, use [`POST .../paragraphs/{paragraph_id}/narrate`](#narrate-a-single-paragraph). To re-render a whole chapter after an edit, `POST .../projects/{project_id}/narration/batch` with `{"chapter_ids": ["<chapter-uuid>"], "include_completed": true}` (add `voice_id` to swap the voice at the same time) — `include_completed` is what makes it a *re*-render, since the chapter's paragraphs are already complete and would otherwise all be skipped. To re-queue only the paragraphs of a chapter whose status is `failed`, `POST .../projects/{project_id}/chapters/{chapter_id}/retry-failed-paragraphs`.

### Cancel a narration run

`POST /audiobook/projects/{project_id}/narration/cancel` stops a narration run that is still in progress. No request body is required.

Cancelling is **resume-friendly, not destructive**:

* Paragraphs that already finished keep their audio — nothing you have paid for is discarded.
* Paragraphs that had not produced audio yet are returned to a re-narratable state, and the charges held for them are released.
* To carry on later, call `POST .../narration/batch` again: it skips the paragraphs that are already done and picks up only what is left.

```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/audiobook/projects/{project_id}/narration/cancel" \
  -H "X-API-Key: $AUDIOPOD_API_KEY"
```

If no narration is running, the endpoint returns `404` with `detail.error_code = "no_narration_running"`. That is also what a **repeat** cancel returns — so a client that retries after a network timeout can treat this as confirmation that the earlier cancel succeeded, not as an error.

If you have a webhook endpoint registered, cancelling emits a [`job.cancelled`](#completion-webhooks) event, so an integration waiting on a narration job learns it has stopped instead of polling until it times out.

<Note>
  Existing webhook endpoints only receive the events they were registered with. If yours was created before `job.cancelled` existed, re-register it (or add the event) to receive cancellations.
</Note>

### Regenerate a completed chapter

By default, `POST .../narration/batch` **skips** paragraphs that already have completed narration — that's the `"No eligible paragraphs to narrate"` response. To re-narrate them, pass `"include_completed": true`:

```json theme={null}
{
  "voice_id": 389,
  "chapter_ids": ["<chapter-uuid>"],
  "include_completed": true
}
```

That body — one `chapter_ids` entry plus `include_completed` — *is* the "re-render one chapter" call. There is no separate endpoint for it; the old `.../chapters/{chapter_id}/regenerate` route is deprecated and returns `410 Gone`.

## 6. Production extras (optional)

Add intro/outro music, narrated opening/closing credits, or a cover before export:

* `GET·PUT /audiobook/projects/{project_id}/production-settings` — `ProductionMusicSettings` (`intro`, `outro`, `credits`, `cover`, `silence`).

  **This is where the spoken title/author announcement is configured.** Set `credits.narrator_name` (plus `include_opening` / `include_closing`, both default `true`) and the export produces narrated `Chapter_00_*_Credits.mp3` (title, subtitle, author, narrator) and `Chapter_99_*_Credits.mp3` as separate files in the ACX package — the opening announcement Audible/ACX expect. **No credits files are produced unless this is set.**

  ```json theme={null}
  PUT /audiobook/projects/{project_id}/production-settings
  {"credits": {"narrator_name": "Jane Doe", "include_opening": true, "include_closing": true}}
  ```

  <Note>Previously named `/music-settings`. That path still works and will keep working, but it only ever described 2 of the 4 fields — use `production-settings`.</Note>
* `POST /audiobook/projects/{project_id}/media/upload` — upload a cover image or custom music track to reference from the settings above.

### Chapter silence (room tone)

Every exported chapter opens and closes with digital silence — this is why an
exported MP3 has leading and trailing silence, and it is a retail requirement,
not padding you should trim. The same values apply to the per-chapter download
and to each chapter inside the ACX package, so an audition always matches what
ships.

Defaults are **750 ms head / 3000 ms tail** (mid-window for ACX). Override per
project with `silence`:

```json theme={null}
PUT /audiobook/projects/{project_id}/production-settings
{"silence": {"head_ms": 2000, "tail_ms": 2000}}
```

<ResponseField name="silence.head_ms" type="integer" default="750">Leading silence, in milliseconds. Accepted range **0–5000**.</ResponseField>
<ResponseField name="silence.tail_ms" type="integer" default="3000">Trailing silence, in milliseconds. Accepted range **0–10000**.</ResponseField>
<ResponseField name="silence.acx_compliant" type="boolean">Read-only. `true` when `head_ms` is 500–1000 and `tail_ms` is 1000–5000 — the ACX opening/closing silence requirements.</ResponseField>

`silence` is always returned, populated with the defaults when you have never
set it, so you can read the effective values without knowing them.

Values outside the **ACX** range are **accepted, not rejected** — plenty of
deliveries are not Audible, and a house standard of 2 s/2 s is a legitimate
choice. Each out-of-range field adds an entry to the response's `warnings`
array naming the field and the ACX range, and `acx_compliant` flips to `false`:

```json theme={null}
{ "silence": { "head_ms": 2000, "tail_ms": 2000, "acx_compliant": false },
  "warnings": ["head_ms 2000 is outside the ACX range 500–1000 ms; files may be rejected by ACX QA."] }
```

Values outside the **accepted range** (a negative `head_ms`, a `tail_ms` above
10000\) are a `422`. Send `"silence": null` to reset both sides to the defaults;
omit the field entirely to leave them unchanged.

<Warning>If you are delivering to ACX/Audible, keep `acx_compliant` at `true`. Out-of-range silence is one of the most common causes of a retail QA rejection, and the rejection arrives after upload — not at export time.</Warning>

## 7. Export the ACX package

`POST /audiobook/projects/{project_id}/export` runs an automatic ACX compliance check, then assembles the package.

<ParamField body="include_chapters" type="array">Optional — export only these chapters.</ParamField>
<ParamField body="custom_settings" type="object">Optional — override export settings.</ParamField>

**Response** (`ACXExportResponse`):

<ResponseField name="job_id" type="integer">Export job ID.</ResponseField>
<ResponseField name="compliance_check" type="object">An `ACXComplianceCheck` (see below).</ResponseField>
<ResponseField name="credits_tts_cost" type="integer">Any additional TTS cost incurred by the export (0 when audio already exists).</ResponseField>

**`ACXComplianceCheck`:**

<ResponseField name="is_compliant" type="boolean">Whether the book passes ACX requirements.</ResponseField>
<ResponseField name="issues" type="array">Blocking problems (export is refused when non-empty).</ResponseField>
<ResponseField name="warnings" type="array">Non-blocking advisories.</ResponseField>
<ResponseField name="file_format_ok" type="boolean">Container/bitrate compliant.</ResponseField>
<ResponseField name="audio_levels_ok" type="boolean">RMS/peak within ACX bounds.</ResponseField>
<ResponseField name="silence_requirements_ok" type="boolean">Head/tail room present.</ResponseField>
<ResponseField name="file_naming_ok" type="boolean">File names follow the ACX convention.</ResponseField>

If a book is non-compliant, the call returns the specific issues instead of producing an invalid package:

```json theme={null}
{ "detail": { "message": "Project is not ACX-compliant. Fix these issues first.",
              "issues": ["Chapter 3: 28s < ACX minimum 30s"], "warnings": [] } }
```

Poll `GET .../export/{job_id}/status` until `COMPLETED`, then fetch the download:

`GET /audiobook/projects/{project_id}/export/{job_id}/download` returns a JSON manifest with a **7-day presigned `download_url`** to the ZIP:

```json theme={null}
{ "download_url": "https://media.audiopod.ai/...acx_package_xxx.zip?...",
  "file_size_bytes": 2079760, "file_format": "zip", "chapter_count": 2 }
```

### The ACX package

| File                          | Contents                                                    |
| ----------------------------- | ----------------------------------------------------------- |
| `Chapter_NN.mp3`              | One per chapter — MP3, **192 kbps CBR, 44.1 kHz**           |
| `00_retail_sample.mp3`        | Retail / audition sample                                    |
| `Chapter_00/99_*_Credits.mp3` | Optional narrated opening/closing credits (when configured) |
| `metadata.json`               | Title, author, narrator, per-chapter durations, format spec |
| `manifest.csv`                | Flat file list                                              |
| `provenance.json`             | Generation provenance                                       |

## Completion webhooks

Rather than poll, register an endpoint and receive a signed event the moment a job finishes — the recommended pattern for backlog-scale automation.

| Operation         | Method | Endpoint                                       |
| ----------------- | ------ | ---------------------------------------------- |
| Register endpoint | POST   | `/webhooks/endpoints`                          |
| List endpoints    | GET    | `/webhooks/endpoints`                          |
| Send a test event | POST   | `/webhooks/endpoints/{endpoint_id}/test`       |
| Delivery log      | GET    | `/webhooks/endpoints/{endpoint_id}/deliveries` |
| Redeliver         | POST   | `/webhooks/deliveries/{delivery_id}/redeliver` |

```bash theme={null}
curl -X POST "$AP/webhooks/endpoints" -H "X-API-Key: $AUDIOPOD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.com/hooks/audiopod","events":["job.completed","job.failed","job.cancelled"]}'
# => { "id": "...", "url": "...", "events": [...], "secret": "whsec_..." }   # secret shown ONCE
```

Every delivery carries these headers:

| Header                 | Meaning                                                                |
| ---------------------- | ---------------------------------------------------------------------- |
| `X-AudioPod-Signature` | `sha256=<hex>` HMAC of the payload (see below)                         |
| `X-AudioPod-Timestamp` | Unix seconds; part of the signed material (replay protection)          |
| `X-AudioPod-Event`     | `job.completed`, `job.failed`, or `job.cancelled`                      |
| `X-AudioPod-Event-Id`  | Stable UUID per event — **dedupe on this** (delivery is at-least-once) |

**Verify the signature** — HMAC-SHA256 over `"<timestamp>.<raw-body>"` with your endpoint secret as the key:

```python theme={null}
import hmac, hashlib

def verify(secret: str, raw_body: bytes, signature_header: str, timestamp_header: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        timestamp_header.encode() + b"." + raw_body,   # exact raw bytes received
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature_header)
```

Delivery is retried with exponential backoff and parked in a dead-letter queue after repeated failures; inspect and redeliver from the delivery log. Endpoints are SSRF-guarded (the resolved IP is re-checked at delivery time; https-only; private/internal ranges blocked).

## Status values

<ResponseField name="Project status" type="string">
  `draft` → `parsing` → `ready_for_narration` → `narrating` → `ready_for_production` → `mixing` → `ready_for_export` → `exporting` → `completed`. `failed` if a stage cannot complete (with the reason in `error_message`).

  Note that the state *after a successful parse* is `ready_for_narration`, not `parsing` or `completed` — pair it with `parsing_completed_at` when waiting on a parse.
</ResponseField>

<ResponseField name="Chapter / paragraph status" type="string">
  `pending` → `queued` → `narrating` → `completed`, or `failed` with an `error_message`.
</ResponseField>

Jobs are prepaid: credits are reserved at job start, settled on success, and **released on failure** — a failed narration or export never leaves a stranded charge. Narration and export retry automatically; a `job.failed` webhook fires if a job ultimately can't complete, so an automated integrator can react deterministically.

## Error Handling

<AccordionGroup>
  <Accordion title="400 — Project is not ACX-compliant">
    The export found blocking issues (e.g. a chapter under the 30-second minimum). The response lists each issue under `detail.issues`. Fix the flagged chapters (lengthen, merge, or `skip_narration`) and re-export.
  </Accordion>

  <Accordion title="A chapter failed narration (content too short)">
    Chapters under \~50 characters can't be narrated (often a stray title-page fragment from PDF parsing). Mark it `skip_narration: true` via `PUT .../chapters/{id}`, or merge it into an adjacent chapter, then continue.
  </Accordion>

  <Accordion title="402 — Insufficient balance">
    Narration/export draws from your prepaid balance. Top up the [API Wallet](/account/api-wallet) and retry; reserved credits for a failed job are released automatically.
  </Accordion>

  <Accordion title="409 — Something else is already running for this project">
    Parsing, narration, mixing, and export are mutually exclusive per project — each would rewrite structure the other is reading. Poll `GET .../narration/progress` (or the project's `status`) and retry once the live job finishes. A `409` on a single paragraph means it is `locked`; unlock it via `PUT .../paragraphs/{paragraph_id}` first.
  </Accordion>

  <Accordion title="410 — narration/start is gone">
    `POST .../narration/start` is deprecated and removed. Send `POST .../narration/batch` instead — with no `chapter_ids` and no `paragraph_ids` it narrates the whole project.
  </Accordion>

  <Accordion title="410 — chapters/{chapter_id}/regenerate is gone">
    Deprecated and removed. Send `POST .../narration/batch` with `{"chapter_ids": ["<chapter-uuid>"], "include_completed": true}`. Without `include_completed` the request succeeds but narrates nothing — every paragraph in an already-rendered chapter is filtered out as complete, and you get `400 No eligible paragraphs to narrate`.
  </Accordion>

  <Accordion title="Pagination appears stuck on page 1">
    You are probably sending `skip`/`limit`. Unknown query parameters are ignored rather than rejected, so the request still returns `200` — with the first page, every time. The project and chapter list endpoints page with `page` and `per_page`; loop on `has_next`.
  </Accordion>

  <Accordion title="401 — Not authenticated">
    Send a valid `X-API-Key` header. Keys are created in the [dashboard](https://www.audiopod.ai/dashboard/account/api-keys) and can expire — check the key's status if previously working calls start returning 401.
  </Accordion>
</AccordionGroup>

## Pricing

Usage-based on a prepaid wallet — you pay per minute of finished output, with no per-seat fee or monthly minimum.

| Service                    | Rate                   |
| -------------------------- | ---------------------- |
| Narration (text-to-speech) | \$0.04 / min of output |
| Voice cloning              | \$0.04 / min of output |
| Transcription (if used)    | \$0.01 / min           |

A 10-hour audiobook (\~600 minutes) costs roughly \*\*$24**, versus $2,000–\$4,000 for a human ACX narrator. For committed monthly volume across many titles, [contact us](mailto:sales@audiopod.ai) about partner pricing below list. See [API Wallet](/account/api-wallet) for top-ups and balance.

## Next Steps

<CardGroup cols={2}>
  <Card title="Voice Management" icon="user" href="/api-reference/voice-management">
    Create custom narrator voices from a short sample, then narrate with them.
  </Card>

  <Card title="API Wallet" icon="wallet" href="/account/api-wallet">
    Top up, check your balance, and see per-minute pricing.
  </Card>

  <Card title="Authentication" icon="key" href="/api-reference/auth">
    API keys, scopes, and how to authenticate every request.
  </Card>

  <Card title="Quickstart" icon="rocket" href="/quickstart">
    The shortest path from zero to a generated audiobook.
  </Card>
</CardGroup>
