> ## 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.
* **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}`                     |
| 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`                              |
| Regenerate a chapter                                             | POST           | `/audiobook/projects/{project_id}/chapters/{chapter_id}/regenerate`              |
| 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 **removed** and now returns **`410 Gone`**. It predates per-paragraph billing and bypassed metering entirely. 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.
</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" }
```

### 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">Screenplay-style direction (max 500 chars); takes precedence over `narration_style` for this take.</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.

## 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="{}">Free-form per-run narration settings.</ParamField>

**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}/chapters/{chapter_id}/regenerate` (optional `?voice_id=` query parameter). 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
}
```

Alternatively, `POST .../projects/{project_id}/chapters/{chapter_id}/regenerate` re-renders a single chapter in one call.

## 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` has been removed because it bypassed metering. Send `POST .../narration/batch` instead — with no `chapter_ids` and no `paragraph_ids` it narrates the whole project.
  </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>
