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 two 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.
A third 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 other values, file and email, can appear in GET responses for episodes ingested via the web uploader and email-in surfaces, but are read-only — submitting them on POST 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":"…"}'
Successful responses are 201 Created:
{ "id": "ep_aB3xK9mQ2pL7" }
The 201 body is intentionally minimal — audio generation is async and most fields (title, author, duration_seconds) aren't populated yet. Call GET /api/v1/episodes/:id to poll 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_…"
{
"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,
"created_at": "2026-04-19T14:22:09Z"
}
}
Fields
-
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
urlandextensionsources with the normalized URL (tracking params stripped).nullforpaste,file, andemailsources. See Source types. -
source_type enum
One of
url,paste,extension,file, oremail. 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. -
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 extension sources they're provided by the browser extension. All three are present on a complete episode.
Source types
The source_type enum has five values. The API text input maps to the stored value paste. file and email are read-only — they appear only in responses for episodes ingested through the web uploader and email-in surfaces.
-
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 read-only
Created by the web uploader.
source_urlisnull. Submittingsource_type=fileonPOSTreturns422. -
email read-only
Created by the email-in ingestion path.
source_urlisnull. Submittingsource_type=emailonPOSTreturns422.
Status lifecycle
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.
{
"episodes": [
{
"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,
"created_at": "2026-04-19T14:22:09Z"
}
],
"meta": { "page": 1, "limit": 25, "total": 142 }
}
Each element of episodes is the same object shape returned by GET /api/v1/episodes/:id (unwrapped — no episode: key). Results are newest-first. meta includes total; compute has_more as page * limit < total if needed.
Delete an episode
curl -X DELETE https://podread.app/api/v1/episodes/ep_aB3xK9mQ2pL7 \
-H "Authorization: Bearer sk_live_…"
{ "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":"Unauthorized"} |
| 402 | Out of episode credits on POST. upgrade_url points at the web billing page (for humans); agents should treat this as a terminal error or switch to MPP. |
{"error":"Payment required","credits_remaining":0,"subscription_active":false,"upgrade_url":"https://podread.app/billing"} |
| 404 | show/destroy on a nonexistent id or one owned by a different user — PodRead does not leak existence across users. |
{"error":"Episode not found"} |
| 422 | Missing or invalid source_type, or source processing failed (URL unreachable, text too long, etc.) |
{"error":"source_type is required. Use 'url', 'text', or 'extension'."} |
| 429 | Over 20 episode creations in the last rolling hour. Back off and retry after a minute. | {"error":"You've reached your hourly episode limit"} |