Docs

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 three 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.
  3. file - upload a document as a multipart/form-data part named file. PodRead reads .pdf, .docx, .epub, .html, .htm, .rtf, .md, .markdown, and .txt, up to the sizes on Upload a file. Optional title, author, and description. Without them, PodRead reads each one from the document's text. A scanned PDF with no text layer returns 422.

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.

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":"…"}'
file source
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_…"
200 OK
{
  "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, and feed 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, email, or feed. 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.

  • credit_cost integer | null

    The cost in credits. 0 when the free plan pays. null until PodRead reads a url source.

  • approval_url string | null

    The page where the user approves the cost of a draft. null for 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_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 POST-able

    Uploaded document, from the web uploader or a multipart/form-data POST with a file part. source_url is null.

  • email Read-only

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

  • feed Read-only

    Created when a feed subscription finds a new entry. source_url is the entry URL, normalized. Submitting source_type=feed on POST returns 422.

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_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
{
  "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_…"
200 OK
{ "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"}}

Start listening free

2 episodes/month, no credit card required

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