Public API
Copy page as Markdown

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:

  1. url — fetch and extract the article at url. 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 return 422 — see the aside below.
  2. text — synthesize from text. Optional title and author; 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.

url source
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"}'
text source
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_…"
200 OK
{
  "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 url and extension sources with the normalized URL (tracking params stripped). null for paste, file, and email sources. See Source types.

  • source_type enum

    One of url, paste, extension, file, or email. See Source types.

  • audio_url string | null

    Direct URL to the generated MP3 on public object storage. null until status becomes complete. 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; null before.

  • error_message string | null

    Populated only when status is failed; null otherwise.

  • 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_url is the submitted URL, normalized.

  • paste POST-able

    Direct text submission via the API text input. source_url is null.

  • extension Internal

    Reserved for the first-party PodRead browser extension. source_url is the captured page URL, normalized.

  • file read-only

    Created by the web uploader. source_url is null. Submitting source_type=file on POST returns 422.

  • email read-only

    Created by the email-in ingestion path. source_url is null. Submitting source_type=email on POST returns 422.

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_message explains.

List episodes

curl "https://podread.app/api/v1/episodes?page=1&limit=25" \
  -H "Authorization: Bearer sk_live_…"
  • page — 1-indexed page number (default 1).
  • limit — page size. Default 20, capped at 100.
200 OK
{
  "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_…"
200 OK
{ "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"}

Start listening free

2 episodes/month, no credit card required

By signing up, you agree to our Terms and Privacy Policy.