Docs
Episodes
Create, list, fetch, and delete episodes on your primary podcast feed. All endpoints require a bearer token - see Authentication. Requests and responses are JSON; all timestamps are UTC (ISO 8601 with a trailing Z).
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/episodes | Create an episode from a URL, pasted text, or extension payload |
| GET | /api/v1/episodes | List your episodes (paginated, newest first) |
| GET | /api/v1/episodes/:id | Fetch a single episode by its ep_-prefixed id |
| DELETE | /api/v1/episodes/:id | Delete an episode |
Create an episode
POST /api/v1/episodes. The source_type field picks one of three public input paths:
url- fetch and extract the article aturl. The full article body is read (not just the title or metadata) and narrated. Title, author, and description are extracted from the page and cleaned by an LLM pass; you cannot override them. Paywalled or JavaScript-rendered pages may extract poorly and return422- see the aside below.text- synthesize fromtext. Optionaltitleandauthor; if omitted, placeholder values are used. No URL is stored.file- upload a document as amultipart/form-datapart namedfile. PodRead reads.pdf,.docx,.epub,.html,.htm,.rtf,.md,.markdown, and.txt, up to the sizes on Upload a file. Optionaltitle,author, anddescription. Without them, PodRead reads each one from the document's text. A scanned PDF with no text layer returns422.
An optional voice field picks the narrator for any source type. Send an id from GET /api/v1/voices. Without it, PodRead narrates with your saved voice. An id your account cannot use returns 422. The voice you send also sets the credit cost.
An optional max_credits field is the most credits the user agrees to pay. An episode that costs more than max_credits, or more than 1 credit when you send none, returns 201 with status draft, its credit_cost, and an approval_url. The user approves the cost at that page, and PodRead then makes the episode. The "Always charge long episodes without asking" setting skips the draft. A URL has no known length until PodRead reads it, so GET shows its draft. Ask the user before you send it.
A fourth value, extension, is reserved for the first-party PodRead browser extension and is not part of the public API contract - do not build against it. Two more values, email and feed, can appear in GET responses. The email-in surface creates the first, and a feed subscription creates the second. Both are read-only - a POST that submits either one returns 422.
curl -X POST https://podread.app/api/v1/episodes \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"source_type":"url","url":"https://www.worksinprogress.news/p/the-secret-behind-japans-railways"}'
curl -X POST https://podread.app/api/v1/episodes \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"source_type":"text","title":"Weekly notes","author":"Jesse","text":"…"}'
curl -X POST https://podread.app/api/v1/episodes \
-H "Authorization: Bearer sk_live_…" \
-F source_type=file \
-F title="Q3 board update" \
-F file=@q3-board-update.pdf
Successful responses are 201 Created:
{
"object": "episode",
"id": "ep_aB3xK9mQ2pL7",
"title": "Untitled",
"author": "Unknown",
"description": "…",
"status": "pending",
"source_type": "url",
"source_url": "https://www.worksinprogress.news/p/the-secret-behind-japans-railways",
"audio_url": null,
"duration_seconds": null,
"error_message": null,
"credit_cost": null,
"approval_url": null,
"created_at": "2026-04-19T14:22:09Z"
}
The 201 body is the same episode object that GET /api/v1/episodes/:id returns. Audio generation is async, so title, author, audio_url, and duration_seconds hold placeholders or null until status is complete. Poll the id to read the full object.
Polling for completion
Poll the returned id no more than once every 5 seconds. Typical completion is 30–90 seconds; long articles or backlogged queues can take longer. Stop polling when status is complete or failed. Webhooks are not available yet.
Fetch an episode
curl https://podread.app/api/v1/episodes/ep_aB3xK9mQ2pL7 \
-H "Authorization: Bearer sk_live_…"
{
"object": "episode",
"id": "ep_aB3xK9mQ2pL7",
"title": "The article title",
"author": "Jane Doe",
"description": "…",
"status": "complete",
"source_type": "url",
"source_url": "https://www.worksinprogress.news/p/the-secret-behind-japans-railways",
"audio_url": "https://storage.googleapis.com/podread-audio/…/ep_aB3xK9mQ2pL7.mp3",
"duration_seconds": 612,
"error_message": null,
"credit_cost": null,
"approval_url": null,
"created_at": "2026-04-19T14:22:09Z"
}
Fields
-
object string
Always
episode. Names the object type, so a client can tell an episode from a list or an error. -
id string
The
ep_-prefixed public identifier. -
title string
The article title.
-
author string
The article author.
-
description string
A short summary of the content.
-
source_url string | null
The origin URL. Populated for
url,extension, andfeedsources with the normalized URL (tracking params stripped).nullforpaste,file, andemailsources. See Source types. -
source_type enum
One of
url,paste,extension,file,email, orfeed. See Source types. -
audio_url string | null
Direct URL to the generated MP3 on public object storage.
nulluntilstatusbecomescomplete. URL is stable and safe to hand to a podcast player or cache; it does not require the bearer token. -
status enum
Lifecycle state; see Status lifecycle.
-
duration_seconds integer | null
Populated after
complete;nullbefore. -
error_message string | null
Populated only when
statusisfailed;nullotherwise. -
credit_cost integer | null
The cost in credits.
0when the free plan pays.nulluntil PodRead reads aurlsource. -
approval_url string | null
The page where the user approves the cost of a
draft.nullfor any other status. -
created_at timestamp
ISO 8601, UTC, trailing
Z.
title, author, and description share extraction behavior: for url sources they're pulled from the article's page metadata; for text sources they're provided by the client at creation (or placeholder values if omitted); for file sources they're provided by the client, or read from the document's text; for extension sources they're provided by the browser extension; for feed sources they're pulled from the entry's page, the same way as url sources. All three are present on a complete episode.
Source types
The source_type enum has 6 values. The API text input maps to the stored value paste. email and feed are read-only - they appear only in responses. The email-in surface creates email episodes, and a feed subscription creates feed episodes.
-
url POST-able
Server fetches and extracts the article.
source_urlis the submitted URL, normalized. -
paste POST-able
Direct text submission via the API
textinput.source_urlisnull. -
extension Internal
Reserved for the first-party PodRead browser extension.
source_urlis the captured page URL, normalized. -
file POST-able
Uploaded document, from the web uploader or a
multipart/form-dataPOSTwith afilepart.source_urlisnull. -
email Read-only
Created by the email-in ingestion path.
source_urlisnull. Submittingsource_type=emailonPOSTreturns422. -
feed Read-only
Created when a feed subscription finds a new entry.
source_urlis the entry URL, normalized. Submittingsource_type=feedonPOSTreturns422.
Status lifecycle
draft- the episode costs more than the approved credits, or more than the balance. It waits until the user approves the cost or buys credits in PodRead. No job runs.pending- row created, job not yet picked up.preparing- source fetched / extracted; setting up synthesis.processing- audio synthesis running.complete- audio generated, episode visible in the feed.failed- processing hit an unrecoverable error;error_messageexplains.
List episodes
curl "https://podread.app/api/v1/episodes?page=1&limit=25" \
-H "Authorization: Bearer sk_live_…"
page: 1-indexed page number (default1).limit- page size. Default20, capped at100.
{
"object": "list",
"data": [
{
"object": "episode",
"id": "ep_aB3xK9mQ2pL7",
"title": "The article title",
"author": "Jane Doe",
"description": "…",
"status": "complete",
"source_type": "url",
"source_url": "https://www.worksinprogress.news/p/the-secret-behind-japans-railways",
"audio_url": "https://storage.googleapis.com/podread-audio/…/ep_aB3xK9mQ2pL7.mp3",
"duration_seconds": 612,
"error_message": null,
"credit_cost": 1,
"approval_url": null,
"created_at": "2026-04-19T14:22:09Z"
}
],
"has_more": true,
"page": 1,
"limit": 25,
"total": 142
}
Each element of data is the same object that GET /api/v1/episodes/:id returns. Results are newest-first. has_more is true when a later page holds more episodes, and total counts every episode on the account.
Delete an episode
curl -X DELETE https://podread.app/api/v1/episodes/ep_aB3xK9mQ2pL7 \
-H "Authorization: Bearer sk_live_…"
{ "object": "episode", "id": "ep_aB3xK9mQ2pL7", "deleted": true }
Deletion removes the episode from your feed and queues cleanup of its audio file. DELETE returns 200 OK with a confirmation body rather than 204 No Content so clients can assert on the deleted: true flag.
Error responses
| Status | When | Body |
|---|---|---|
| 401 | Missing or invalid bearer token | {"error":{"type":"authentication_error","code":"invalid_token","message":"Unauthorized"}} |
| 402 | Out of episode credits on POST. upgrade_url is a web page for a person: the single episode offer when the free episodes are used up, or the billing page when the credits are too few for the episode; agents should treat this as a terminal error or switch to MPP. |
{"error":{"type":"payment_error","code":"insufficient_credits","message":"Payment required","credits_remaining":0,"paid_user":false,"upgrade_url":"https://podread.app/episodes/new"}} |
| 404 | show/destroy on a nonexistent id or one owned by a different user - PodRead does not leak existence across users. |
{"error":{"type":"invalid_request_error","code":"resource_missing","message":"Episode not found","param":"id"}} |
| 422 | Missing or invalid source_type, or source processing failed (URL unreachable, text too long, etc.) |
{"error":{"type":"invalid_request_error","code":"parameter_missing","message":"source_type is required. Use 'url', 'text', 'file', or 'extension'.","param":"source_type"}} |
| 429 | Over 20 episode creations in the last rolling hour. Back off and retry after a minute. | {"error":{"type":"rate_limit_error","code":"rate_limited","message":"You've reached your hourly episode limit"}} |