The Askwho Casts API

Everything the website does, for scripts and AI agents: a REST API and an MCP server.

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.

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:

{"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

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:

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

Your own text, one narrator

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):

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:

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:

{"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:

{"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:

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}.

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:

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:

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:

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:

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:

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

Size limits:

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= (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:.
PUT /api/v1/books/position Save the listening place: {bookKey, track, seconds}.
GET /api/v1/books/bookmarks Bookmarks in a book: ?bookKey=doc:.
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= (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:", 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?}.