# The Askwho Casts API

Askwho Casts turns articles, your own text and whole books into multi-voice
audio: a narrator, and a distinct voice for every person quoted. Episodes
land in the person's private podcast feed. This API does everything the
website does, for scripts and AI agents: no browser and no page scripting.

- **Base URL:** `https://askwhocasts.com/api/v1`
- **Auth:** `Authorization: Bearer awc_…`, a personal API key. The person
  makes one at https://askwhocasts.com/account/settings, under **API keys**
  (keys come with a plan). The Askwho Casts app signs in with its own token
  (`awca_…`), which reaches the same endpoints.
- **Format:** JSON in, JSON out. Field names are camelCase.
- **This guide:** `GET /api/v1/docs`. The endpoint index is `GET /api/v1`.
  The OpenAPI document is `GET /api/v1/openapi.json`. None of these needs a key.
- **MCP:** `https://askwhocasts.com/mcp` (Streamable HTTP, same key). See "Use it from Claude" below.
- **Feedback:** `POST /api/v1/feedback`. If something here is clumsy, confusing
  or missing, say so. The person who runs Askwho Casts reads every note.

## Use it from Claude

**Claude Code**, as an MCP server (the tools appear on their own):

```
claude mcp add --transport http askwhocasts https://askwhocasts.com/mcp \
  --header "Authorization: Bearer awc_YOUR_KEY"
```

**Any agent that can make HTTP requests:** give it the key and this sentence:

> Use the Askwho Casts API at https://askwhocasts.com/api/v1 with the header
> "Authorization: Bearer awc_YOUR_KEY". Read https://askwhocasts.com/api/v1/docs first.

**Other MCP clients:** a Streamable HTTP server at `https://askwhocasts.com/mcp`
with the header `Authorization: Bearer awc_…`. It's stateless: no session and
no event stream.

## The basics

**Credits.** Recording costs credits: about 1 credit per minute of audio
(1 per ~1,050 characters; books are 1.25 per minute). `GET /account` shows the
balance, and the plan with its `interval` (`month` or `year`: a yearly plan's
credits all arrive when it starts or renews). Only a person can add credits or
change the plan, at https://askwhocasts.com/account/plan.

**Warnings.** `GET /account` returns `warnings`: things the account holder
should hear about now, such as a payment that didn't go through and the date
the plan will be cancelled if it still doesn't. When it isn't empty, tell the
person. Only they can update the card.

**maxCredits.** Every call that spends credits takes an optional `maxCredits`.
If the price is higher, nothing is charged and you get an `over_max_credits`
error with the price in `details`. If you leave it out, the call accepts the
price it just worked out. Ask the person before spending more than they'd expect.

**Waiting.** Recording takes minutes. Calls that start something slow, and the
calls that read its progress, take `wait`: `?wait=60` on a GET, or `"wait": 60`
in a body. The call then holds up to 60 seconds for the work to finish. Without
it, poll every 15–30 seconds.

**Errors.** Every failure has the same shape:

```json
{"error": {"type": "insufficient_credits", "status": 402,
  "message": "This needs 31 credits and you have 12.",
  "hint": "Not enough credits. A person can top up …", "code": "…", "details": {…}}}
```

Branch on `type`. Show `message` to the person if they need to act.

| type | status | meaning |
|---|---|---|
| `invalid_request` | 400 | The request is malformed or missing something. The message says what. |
| `authentication_failed` | 401 | No key, or the key was revoked. |
| `needs_subscription` | 402 | The account has no active plan. |
| `insufficient_credits` | 402 | Not enough credits for this. |
| `over_max_credits` | 402 | The price is above your `maxCredits`. Nothing was charged. |
| `forbidden` | 403 | Not allowed (for example, a feature that isn't on for this account). |
| `not_found` | 404 | No such thing in this account. |
| `conflict` | 409 | Something changed (re-read it and retry), or it's busy (recording, casting). |
| `content_refused` | 422 | The text breaks the content rules. |
| `unprocessable` | 422 | The page couldn't be read. The message says why; sending `html` often helps. |
| `rate_limited` | 429 | Too many requests. Wait and retry. |
| `server_error`, `unavailable` | 5xx | Our side. Retry later. Check before re-sending anything that spends credits. |

**Ids.** An episode has an `id` (a number) and an `articleId` (64 hex
characters). Episode endpoints accept either. Drafts, documents and runs have
string ids.

**Links.** A ready episode has `links.audio`, `links.download`,
`links.transcript`, `links.captions`, `links.chapters` and `links.artwork`.
These are private capability links: anyone holding one can play it, and no key
is needed. `links.audio`, `links.download` and `links.chapters` name the
render they were made for. Once the audio is replaced they answer 410, so fetch
the episode again for fresh links. The account's podcast feed is `feed.url`
in `GET /account`.

**What a player needs.** Each episode also has `readyAt`, `updatedAt`,
`durationSeconds`, `audioBytes`, `chapterCount` and `hasChapters` (whether
`links.chapters` has a file to give), and `renderId`. The render id changes
whenever the audio is replaced (a re-record), so a downloaded
copy whose render id differs is stale.

## Recipes

### Record an article

```http
POST /api/v1/episodes
{"url": "https://example.com/a-long-read", "maxCredits": 40, "wait": 60}
```

This returns `{episode, charged, quote}`. While `episode.status` is
`generating`, poll `GET /api/v1/episodes/{id}?wait=60` until it's `ready`
(or `failed`; a failed recording is refunded automatically). Most articles take
2–10 minutes.

To see the price and the speakers first, call `POST /api/v1/quotes` with
`{"url": …}`. Then send the quote's `sourceSnapshotId` and `maxCredits` to
`POST /episodes`. That records exactly what was quoted.

### A page the site can't fetch (paywalled, signed in)

If you can see the page, send its HTML:

```http
POST /api/v1/episodes
{"url": "https://paywalled.example/story", "html": "<!doctype html>…", "maxCredits": 30}
```

### Your own text, one narrator

```http
POST /api/v1/episodes
{"title": "Notes for Tuesday", "text": "…", "narratorVoice": "<voiceId>"}
```

Leave out `narratorVoice` to have one chosen. To find voices:
`GET /api/v1/voices?gender=f&accent=british&narrator=true`. Each voice has a
`previewUrl` for its sample.

### Choose voices for an article

`POST /quotes` lists `speakers`, each with a `role`, a `name` and the
`voiceId` it would get. To change some, send `cast` (speaker role or name → voice id):

```http
POST /api/v1/episodes
{"url": "https://…", "cast": {"Narrator": "<voiceId>", "Sofia Marsh": "<voiceId>"}, "maxCredits": 40}
```

Each speaker needs a different voice.

### Multi-voice from your own text: the Studio

A **draft** is text plus a cast: who reads each span. The narrator reads
anything not given to a character. Here's the whole loop:

1. **Make the draft.** `POST /api/v1/drafts` with `{"title": …, "text": …, "genre": "fiction"}`.
   A narrator voice is chosen if you don't send `narratorVoice`.
2. **Cast it.** `POST /api/v1/drafts/{id}/cast` with `operations` (below). You can also
   send `operations` straight to `POST /drafts`.
3. **Check it.** `GET /api/v1/drafts/{id}/script?format=text` reads it back as
   `NARRATOR: …` / `ALICE: “…”` lines. `GET /drafts/{id}/marks?status=unplaced` lists
   quotations nobody has yet. `GET /drafts/{id}/estimate` gives the price and anything
   that would stop recording (`problems`).
4. **Record it.** `POST /api/v1/drafts/{id}/render` with `{"maxCredits": 20, "wait": 60}`.

Example: every quotation to one character, Alice, created on the spot:

```http
POST /api/v1/drafts/{id}/cast
{"operations": [
  {"op": "assign", "to": "Alice", "select": {"quotes": "all"}, "create": {"gender": "f"}}
]}
```

Example: two speakers, assigned by quotation number. The numbers come from
`GET /drafts/{id}/marks`, which lists each quotation with its text:

```http
{"operations": [
  {"op": "add_character", "name": "Alice", "gender": "f"},
  {"op": "add_character", "name": "Bob", "gender": "m", "voice": "<voiceId>"},
  {"op": "assign", "to": "Alice", "select": {"marks": [0, 2, 4, 7]}},
  {"op": "assign", "to": "Bob", "select": {"marks": [1, 3, 5, 6]}},
  {"op": "assign", "to": "narrator", "select": {"text": "“so to speak”"}}
]}
```

Example: a script written as `ALICE: Where were you?` turns:

```http
{"operations": [{"op": "cast_labels"}]}
```

Every name becomes a character, with its turns and a fitting voice. The labels
themselves aren't read aloud.

**Auto Cast** can do the casting for you. It reads the text, works out who says
each quotation, and gives everyone a voice. It costs 1–2 credits for up to 126,000
characters, and running it again on the same text is free. Anything you assigned
yourself is kept:

```http
POST /api/v1/drafts/{id}/autocast
{"maxCredits": 2, "wait": 60}
```

Then read the script back, and fix anything with `/cast`.

To hear a voice before choosing it, call `POST /api/v1/drafts/{id}/auditions` with
`{"voiceId": …, "character": "Alice"}` (or `"text"`, up to 200 characters).
It returns an MP3 link.

### Casting a draft: the operations

`POST /drafts/{id}/cast` takes `{"operations": [...], "version"?: n}`.

- **Order and failure.** Operations run in order. If one fails, nothing is saved,
  and the error names the operation by its index.
- **Naming characters.** Name a character by its name or an alias (any case), or
  by its id (`"c3"`). Use `"narrator"` for the narrator.
- **Versions.** If you leave out `version`, the change goes onto the newest draft.
- **Results.** The response has a `results` entry per operation, plus the new `summary`.

| op | fields | does |
|---|---|---|
| `add_character` | `name`, `gender?` (f/m), `kind?` (person, group, unnamed, organisation), `note?`, `aliases?`, `voice?` (id or `"auto"`, the default) | Adds a character with a voice that fits and isn't taken. |
| `assign` | `to` (character or `"narrator"`), `select`, `create?` (`{gender, kind, note, voice}`, to add `to` if it's new) | Gives the selected spans to `to`. Anything already there is split around them, never stacked. |
| `set_voice` | `character` (or `"narrator"`), `voice` (id or `"auto"`) | Sets a voice. |
| `cast_labels` | `createMissing?` (default true) | Gives labelled script turns (`NAME: …`) and chat messages to their named speakers. |
| `update_character` | `character`, `name?`, `gender?`, `kind?`, `note?`, `aliases?` | Changes a character's details. |
| `merge_characters` | `from`, `into` | Moves `from`'s lines to `into`; `from`'s name becomes an alias. |
| `remove_character` | `character` | Removes the character; their lines go to the narrator. |
| `confirm` | `select?` | Clears "to check" flags (all of them, or the selected lines'). |

A `select` names **one** of these:

| select | picks |
|---|---|
| `{"quotes": "all"}` / `{"quotes": "unplaced"}` | Every quotation the Studio finds, or only those no one reads yet. Add `"kinds": ["quote", "label", "system"]` to narrow. |
| `{"marks": [0, 3, 4]}` | Quotations by number, from `GET /drafts/{id}/marks`. |
| `{"text": "exact words", "occurrence": 1}` | An exact piece of text (`occurrence` is 1-based, or `"all"`). Add `"wholeQuote": true` to widen it to the quotation it's in. |
| `{"containing": "words"}` | Quotations containing these words (any case). |
| `{"label": "ALICE"}` | Labelled turns with this label. |
| `{"linesOf": "Bob"}` | Every line a character reads now (to move them all). |
| `{"lines": [120, 486]}` | Lines by their start offset (`body.lines[].start`). |
| `{"spans": [{"start": 0, "end": 42}]}` | Exact character ranges (UTF-16 offsets, end exclusive). |

Add `"within": {"start": …, "end": …}` to any selection to limit it to part of the text.

**What counts as a quotation.** The Studio marks speech in curly, straight and
low-high quotation marks, guillemets and corner brackets, and British
single-quote dialogue. It also marks quotations that run across paragraphs,
dashed dialogue, `NAME: …` script turns, chat messages and LitRPG `[system
boxes]`. A mark includes its quotation marks.

**Editing the text later.** `PATCH /drafts/{id}` with `{"text": …}` keeps the
casting where the words didn't change, as the Studio does. A line whose words
mostly changed is flagged to check.

**Series.** A series is a cast book shared by drafts: chapter 12 keeps chapter
1's voices. Make one with `POST /series`, then create drafts with `seriesId`.

### A book (EPUB, DOCX, PDF, text)

1. **Upload it.** Files up to about 8 MB can go as JSON:
   `POST /api/v1/documents` with `{"filename": "book.epub", "contentBase64": "…", "rightsConfirmed": true, "wait": 60}`.
   Text files can send `"text"` instead of `contentBase64`.

   Large files (up to 80 MB) go as raw bytes:

   ```
   curl -X POST https://askwhocasts.com/api/v1/documents \
     -H "Authorization: Bearer awc_…" -H "X-Filename: book.epub" \
     -H "X-Rights-Confirmed: true" --data-binary @book.epub
   ```

   Send `rightsConfirmed: true` only when the person confirms this: *"This is my
   own writing, a public-domain work, or a copy I own without copy protection,
   and the audio is for my own listening."*
2. **Wait for it to be read.** Poll `GET /documents/{id}?wait=60` until
   `document.status` is `ready`. The response has `review.chapters` (each with
   `id`, `title`, `include`, `chars` and `credits`) and `review.totals`.
3. **Arrange the chapters (optional).** `POST /documents/{id}/layout` with one `op` at a time:
   - `{"op": "include", "id": "l003", "include": false}`
   - `{"op": "include-group", "kind": "front", "include": false}`
   - `{"op": "merge", "id": "l004"}`
   - `{"op": "split", "id": "l002", "at": 5120}`. `at` is a paragraph's start, from `GET /documents/{id}/chapters/l002`.
   - `{"op": "rename", "id": "l001", "title": "Prologue"}`
   - `{"op": "notes", "read": true}`
4. **Cast it.** `POST /documents/{id}/cast` with `{"wait": 60}`. This is free, up to 3 a day.
   It finds every character and gives each a voice. Poll `GET /documents/{id}/cast?wait=60`
   until `status` is `ready`.
5. **Change voices (optional).** `POST /documents/{id}/cast/edit` with
   `{"op": {"op": "voice", "character": "Elizabeth Bennet", "voiceId": "…"}}`,
   `{"op": {"op": "narrator", "voiceId": "…"}}`, or `{"op": {"op": "reset"}}`.
   To hear a choice: `POST /documents/{id}/auditions` with `{"character": …, "voiceId": …}`.
6. **Price it.** `GET /documents/{id}/estimate` gives `{credits, ready, blockers}`.
7. **Make it.** `POST /documents/{id}/make` with `{"maxCredits": 500}`. Then poll
   `GET /documents/{id}/production?wait=60`. Each chapter is recorded into the
   book's own private feed (`feedUrl`). When it's done, `downloads` has
   MP3, ZIP and M4B links. A long book takes hours.

Changing the chapters after casting means casting again.

### Keep a copy of the library in step (an app)

List it once with `GET /api/v1/episodes?limit=200` (page with `offset` while
`offset + limit < total`) and keep the answer's `now`. Next time, send
`GET /api/v1/episodes?updatedSince=<that now>&limit=200`. It lists only the
episodes that changed since then (an episode being made always counts), and adds:
- `ids`: every episode id in the library now. Drop any you hold that isn't in it;
- `deleted`: `[{id, articleId, deletedAt}]`, the episodes taken out since then;
- `now`: the `updatedSince` for next time.

The window overlaps by a few seconds, so an episode can come twice. Treat each as
the newest copy.

### Listening places

`PUT /api/v1/places` saves where the person is, up to 200 at a time:

```http
PUT /api/v1/places
{"places": [{"itemKey": "ep:<articleId>", "seconds": 312.5, "duration": 1800, "played": false, "at": 1792839123000}]}
```

`at` is when they listened, in milliseconds. A save only replaces one with an
older `at`, so phones and browsers can save in any order. The answer is the
stored place for each key. `GET /api/v1/places?since=<now>` lists the places
that changed since your last read (all of them without `since`), with `now`
for next time. Leaving out `played` means not played; leaving out `duration`
keeps the stored one.

### Follow a blog

To see a blog before following it: `POST /following/preview` with `{"url": …}`.

To follow it: `POST /following` with `{"url": …, "monthly_limit": 50, "post_limit": 30}`.
Its new posts are then recorded by themselves, within those limits:
- `monthly_limit` is the most credits a month for this blog;
- `post_limit` is the most credits for one post.

`GET /following` shows the blogs, the spending and recent activity.

## What a key can't do

A key does everything a signed-in person can do with their library, Studio and
books. It can't:
- make or revoke keys;
- change the plan or buy credits;
- give the main podcast feed a new address;
- delete the account;
- use the free first article (that needs a person on the website). Asking for it
  through the API is a `needs_subscription` error with `code`
  `free-article-on-website`, and nothing is charged.

Those stay on the website. Keep the key secret. If one leaks, the person revokes
it in Settings, and it stops working at once.

## Limits

These are the website's own limits:
- at most 5 recordings running at once;
- quotes and page sends: about 30 per 10 minutes;
- auditions: 30 new per draft per day;
- book casts: 3 per day;
- Auto Cast: 10 runs per draft per day;
- uploads: 3 documents an hour.

On top of those, each key can make 600 changes per 10 minutes. Reads aren't limited.

Size limits:
- an article or draft: up to 350,000 characters;
- a draft: up to 120 characters (speakers) and 1,500 lines;
- an upload: up to 80 MB.

## Endpoints

### Start

| Endpoint | What it does |
|---|---|
| `GET /api/v1` | This index: every endpoint, the guide and the MCP address. No key needed. |
| `GET /api/v1/docs` | The guide, in Markdown, written for agents. No key needed. |
| `GET /api/v1/openapi.json` | This API as an OpenAPI 3.1 document. No key needed. |

### Account

| Endpoint | What it does |
|---|---|
| `GET /api/v1/account` | Who the key belongs to: plan, credit balance, private podcast feed, what's switched on, warnings to pass on. |
| `GET /api/v1/credits` | The credit ledger, newest first (the last 100 changes). |
| `GET /api/v1/settings` | The account's settings (chooseVoices, directInput). |
| `PATCH /api/v1/settings` | Change settings: {chooseVoices?, directInput?}. |

### Voices

| Endpoint | What it does |
|---|---|
| `GET /api/v1/voices` | The voice pool. Filter with ?gender=f\|m&age=young\|adult\|middle\|old&accent=british\|american&narrator=true&q=words. |
| `GET /api/v1/voices/:id/preview` | A voice's sample, as MP3. |
| `POST /api/v1/voices/suggest` | Three to five fitting voices for a character: {character: {name, gender, kind, note}, genre, narratorVoice, taken}. |

### Episodes

| Endpoint | What it does |
|---|---|
| `POST /api/v1/quotes` | Price a link before recording it: {url}, or {url, html} for a page you already have (paywalled, signed in). Returns the price, speakers and a 24-hour snapshot. |
| `POST /api/v1/episodes` | Record an episode in one call: {url} (or {url, html}, or {title, text}), optional maxCredits, cast, describeImages, wait. *(spends credits, wait)* |
| `GET /api/v1/episodes` | The library, newest first: ?status=generating\|ready\|failed&kind=link\|page\|script\|document&limit=&offset=. ?updatedSince=<ms> (the last answer's now) lists only what changed, with ids (all of the library) and deleted. |
| `GET /api/v1/episodes/:id` | One episode (by id or articleId): status, progress, and audio/transcript links when ready. *(wait)* |
| `DELETE /api/v1/episodes/:id` | Take an episode out of the library and the feed. |
| `GET /api/v1/episodes/:id/transcript` | The timed transcript: every line, who says it, when. |
| `GET /api/v1/episodes/:id/cast` | Who's in the episode and their voices. |
| `POST /api/v1/episodes/:id/issues` | Report a problem with an episode: {category: speaker\|missing\|description\|audio\|other, seconds?, note?}. |
| `POST /api/v1/episodes/:id/studio` | Open a script episode as a Studio draft again, to edit and re-record. |

### Studio drafts

| Endpoint | What it does |
|---|---|
| `GET /api/v1/drafts` | Your Studio drafts, newest first. |
| `POST /api/v1/drafts` | Make a draft: {title, text, genre?: fiction\|nonfiction, seriesId?, narratorVoice?, operations?}. A narrator is chosen if you don't name one. |
| `POST /api/v1/drafts/from-episode` | A draft from a recorded episode: {episodeId} or {articleId}. |
| `GET /api/v1/drafts/:id` | A draft: its body, a summary (cast, quotations placed, problems, price). ?include=marks,script adds those. |
| `PUT /api/v1/drafts/:id` | Replace a draft's whole body: {version?, body}. |
| `PATCH /api/v1/drafts/:id` | Change {title?, text?, genre?, seriesId?}. Editing the text keeps the casting, as the Studio does. |
| `DELETE /api/v1/drafts/:id` | Delete a draft. |
| `GET /api/v1/drafts/:id/marks` | Every quotation the Studio finds, numbered, with who reads it now (or unplaced). ?status=unplaced filters. |
| `GET /api/v1/drafts/:id/script` | The draft as it will be heard: each piece and its speaker. ?format=text for "ALICE: …" lines; ?from=&to= to window it. |
| `POST /api/v1/drafts/:id/cast` | Cast it in one call: {operations: [...]} (add_character, assign, set_voice, cast_labels, merge_characters, …). All or nothing. |
| `GET /api/v1/drafts/:id/estimate` | The price, the take count and anything that would stop recording. |
| `GET /api/v1/drafts/:id/autocast` | Auto Cast's price for this draft, and its last run. |
| `POST /api/v1/drafts/:id/autocast` | Let Auto Cast read the draft and give every quotation a speaker and a voice: {maxCredits?, wait?}. *(spends credits, wait)* |
| `GET /api/v1/autocast/:runId` | An Auto Cast run. *(wait)* |
| `POST /api/v1/drafts/:id/auditions` | Hear a voice read a line: {voiceId, text} or {voiceId, character}. Returns an MP3 link. |
| `GET /api/v1/auditions/:file` | An audition, as MP3. |
| `POST /api/v1/drafts/:id/render` | Record the draft: {maxCredits?, wait?}. Returns the episode. *(spends credits, wait)* |

### Series

| Endpoint | What it does |
|---|---|
| `GET /api/v1/series` | Your series (cast books shared by drafts). |
| `POST /api/v1/series` | Make a series: {name, genre?}. |
| `GET /api/v1/series/:id` | A series and its characters. |
| `PATCH /api/v1/series/:id` | Rename it or set its narrator: {name?, narratorVoice?}. |
| `DELETE /api/v1/series/:id` | Delete a series (drafts keep their casts). |
| `PATCH /api/v1/series/:id/characters/:key` | Change a series character: {name?, aliases?, gender?, note?, voiceId?}. |
| `DELETE /api/v1/series/:id/characters/:key` | Remove a series character. |
| `POST /api/v1/series/:id/merge` | Merge two series characters: {from, into}. |
| `POST /api/v1/series/:id/restore` | Undo a series change: {undo}. |

### Books

| Endpoint | What it does |
|---|---|
| `POST /api/v1/documents` | Upload a book or document (EPUB, DOCX, PDF, TXT, Markdown, HTML): JSON {filename, text \| contentBase64, rightsConfirmed: true}, or raw bytes with X-Filename and X-Rights-Confirmed: true. *(wait)* |
| `GET /api/v1/documents` | Your documents and books. |
| `GET /api/v1/documents/:id` | A document: when ready, its chapters (review) and layout. *(wait)* |
| `GET /api/v1/documents/:id/chapters/:chapter` | A chapter's text (?full=1 for every word). |
| `POST /api/v1/documents/:id/layout` | Arrange chapters: {op: include\|include-group\|merge\|split\|rename\|notes\|citations\|reset}. |
| `POST /api/v1/documents/:id/title` | Rename the book: {title}. |
| `DELETE /api/v1/documents/:id` | Remove a document, its book and its audio. |
| `POST /api/v1/documents/:id/cast` | Cast the book: find every character and give each a voice (free). *(wait)* |
| `GET /api/v1/documents/:id/cast` | The book's cast sheet. *(wait)* |
| `POST /api/v1/documents/:id/cast/edit` | Change the cast: {op: {op: voice, character, voiceId} \| {op: narrator, voiceId} \| {op: reset, character?}}. |
| `POST /api/v1/documents/:id/auditions` | Hear a voice read a character's line: {character \| "narrator", voiceId}. |
| `GET /api/v1/documents/:id/auditions/:file` | A book audition, as MP3. |
| `GET /api/v1/documents/:id/estimate` | What making the book costs now, and anything in the way. |
| `POST /api/v1/documents/:id/make` | Make the audiobook: {maxCredits?, wait?}. *(spends credits, wait)* |
| `GET /api/v1/documents/:id/production` | The book's progress, chapter by chapter, its feed and downloads (MP3, ZIP, M4B). *(wait)* |
| `GET /api/v1/documents/:id/listen` | The book's tracks, with audio links and the saved listening place. |
| `POST /api/v1/documents/:id/feed/rotate` | Give the book's private feed a new address (the old one stops). |
| `GET /api/v1/books/position` | The saved listening place: ?bookKey=doc:<id>. |
| `PUT /api/v1/books/position` | Save the listening place: {bookKey, track, seconds}. |
| `GET /api/v1/books/bookmarks` | Bookmarks in a book: ?bookKey=doc:<id>. |
| `POST /api/v1/books/bookmarks` | Add a bookmark: {bookKey, track, seconds, note?}. |
| `DELETE /api/v1/books/bookmarks/:id` | Delete a bookmark. |

### Listening places

| Endpoint | What it does |
|---|---|
| `GET /api/v1/places` | Where you are in each episode: ?since=<ms> (the last answer's now) for only what changed. {places: [{itemKey, seconds, duration, played, at, device}], now}. |
| `PUT /api/v1/places` | Save places, the newest listen winning: {places: [{itemKey: "ep:<articleId>", seconds, duration?, played?, at}]}, up to 200. |

### Following blogs

| Endpoint | What it does |
|---|---|
| `GET /api/v1/following` | The blogs you follow, their limits and recent activity. |
| `POST /api/v1/following/preview` | Look at a blog's feed before following it: {url}. |
| `POST /api/v1/following` | Follow a blog: new posts become episodes. {url, monthly_limit?, post_limit?, describe_images?, title_include?, title_exclude?}. *(spends credits)* |
| `PATCH /api/v1/following/settings` | Spending limits for following: {monthly_limit, daily_limit, protected_balance, version}. |
| `POST /api/v1/following/check` | Check every followed blog for new posts now. |
| `PATCH /api/v1/following/:id` | Pause, resume, remove or change a followed blog: {version, status?, …}. |
| `POST /api/v1/following/:id/check` | Check one followed blog now. |
| `POST /api/v1/following/items/:id/skip` | Skip a post. |
| `POST /api/v1/following/items/:id/review` | Take a post to the Studio instead of recording it automatically. |

### Library

| Endpoint | What it does |
|---|---|
| `GET /api/v1/serials` | Serials and series in the library, with their episodes and feeds. |
| `GET /api/v1/classics` | The Classics shelf: handmade audiobooks, and which you can listen to. |
| `GET /api/v1/classics/purchases` | Classics you've bought. |

### Feedback

| Endpoint | What it does |
|---|---|
| `POST /api/v1/feedback` | Tell the people who make Askwho Casts what was clumsy, missing or broken: {message, category?: bug\|missing\|confusing\|idea\|praise\|other, endpoint?}. |
