The episode stream is an Enterprise feature. Access requires an API key belonging to an organization on the Enterprise plan. Authenticate exactly as you do elsewhere — the
X-API-Key header (recommended) or an Authorization: Bearer token. A key on any other plan is rejected with 403 enterprise_required; poll GET /v1/podcasts/episodes/feed instead.podcast_ids set — a long list would exceed query-string limits on a GET. The two forms are otherwise identical.
Pick a milestone
Episodes move through ingestion in stages. You subscribe to exactly one milestone and receive each episode once, when it reaches that stage. The milestones are strictly ordered — each builds on the previous — so picking a later milestone means you wait longer but the episode arrives with more data already populated.
If you don’t pass
milestone, you get transcribed. Choose the single milestone that matches the data you need — a later one implies all earlier stages already happened. Expect real latency between stages: transcription and enrichment take minutes to hours.
Historical imports are not delivered
The stream carries episodes as we discover them. When a show’s back catalogue is imported in bulk — onboarding a podcast we didn’t previously cover, or extending an existing show’s coverage further back — those older episodes are added to the catalogue but are not delivered here. A show’s 2024 archive landing in your stream today would be indistinguishable from three hundred new releases, and you’d have no way to tell which were actually new. Those episodes are fully available through episode search and the per-podcast episode listings — they’re absent from the change stream, not from the API. If you need a show’s complete history, page its episodes directly; use the stream for what changes from here on.Filter the podcasts
By default the stream delivers every episode in the catalog. Narrow it two ways, which combine as a union (an episode is delivered if it matches either):podcast_ids— an explicit set of podcasts, each given as a slug (pivot) or ID. An episode is delivered if its podcast is in the set. Unknown values are ignored, so a single bad slug won’t break the stream — but if none of the supplied ids match a known podcast, the request fails immediately with anerrorevent rather than leaving you waiting on a stream that can never produce anything.popularity_threshold— a number in(0, 1). Podcast popularity is normalized 0–1 across the catalog (a percentile), so0.9≈ the top 10% most popular podcasts. Use this to follow “the popular stuff” without enumerating ids.
podcast_ids set via the POST body (see below). On GET, podcast_ids is capped at 100; beyond that you’ll get an error event telling you to use POST.
Parameters
milestone, cursor, since, and include are always query parameters. podcast_ids and popularity_threshold are query parameters on GET and JSON body fields on POST.
Open the stream
A simple GET — all transcribed episodes, live:cursor or since, the stream is live-only: you receive episodes that reach your milestone from the moment you connect forward.
Event format
Each message is an SSE event. There are two event types.event: episode — an episode reached your milestone. The data is a JSON envelope:
episode object is the same list-shaped representation returned by list episodes and the feed, hydrated to the level implied by your milestone (has_transcript, segment_count, etc. reflect the stage reached). For the full per-episode detail — topics, all entities, videos — fetch GET /v1/podcasts/episodes/{id}, or embed the heavy relations inline with include.
event: error — a terminal error (e.g. a filter that matched no podcasts, too many ids for a GET, or an invalid cursor). The server sends one and closes the connection:
Hydrate the payload
By default each episode carries only its metadata, counts, and flags (has_transcript, segment_count, clip_count) — the heavy relations are not shipped, so a consumer that only needs to know an episode reached a milestone never pays for transcript bytes. To embed those relations directly — and avoid a follow-up request per delivered episode — pass include:
Combine values with commas:
include=transcript,clips.
A relation can only be embedded at a milestone that guarantees it. Each becomes available at the milestone above, and because milestones are ordered, you can only embed what your milestone has reached. Asking for clips at milestone=transcribed is a contradiction — you’d be woken before clips exist — and is rejected with a terminal error event. all is milestone-relative: it expands to exactly the relations your milestone guarantees, so it never conflicts (e.g. all at transcribed embeds just the transcript).
GET /v1/podcasts/episodes/{id}/transcript/words.
Manage the stream lifecycle
Programming against the stream is mostly about three things: store the cursor, dedupe on episode id, and reconnect.The cursor
Everyepisode event carries an opaque cursor. Treat it as a black box — don’t parse it. Persist the cursor of the last event you have fully processed. It’s your resume point.
Delivery is at-least-once
You may occasionally receive the same episode more than once — most commonly right after a reconnect. Dedupe onepisode.id and make your processing idempotent. You will not silently miss episodes (see below), but you should expect the rare duplicate rather than assume exactly-once.
Resuming after a disconnect
Connections end — network blips, your deploys, our rolling restarts. To resume without gaps, reconnect and pass the last cursor you stored as?cursor=:
?cursor=.) If you’ve never connected before and want history, use since instead of cursor.
Cursors don’t expire, and there’s no maximum lag. The ingestion log behind the stream has no retention window, so a cursor you stored minutes or weeks ago resumes exactly the same way: catch-up replays everything after it, then the stream goes live. A consumer that reads slowly is never disconnected for falling behind — delivery simply paces to how fast you read, and the backlog waits in the log rather than in a server-side buffer. The golden rule is simply always reconnect from your last processed cursor.
since earlier than that starts at the oldest recorded event rather than failing.
A resilient consumer
The pattern in any language: connect → on eachepisode event, dedupe and process, then store its cursor → on error or disconnect, back off and reconnect with the stored cursor. Use exponential backoff with jitter, capped at a ceiling (e.g. 1s → 30s), and reset the delay to its minimum after a connection stays up and delivers — so a routine deploy reconnects within a second or two, while a sustained outage doesn’t hammer the API.
JavaScript
parseSSE is any standard SSE line parser (split on blank lines; read event: and data: fields). Persisting cursor to durable storage lets you resume cleanly across process restarts, not just transient drops.
Stream vs. poll
Not on Enterprise, or prefer polling to a long-lived connection? The episode feed is the all-plans pull alternative — the same episodes, milestones, and filters, returned by a resumableGET you poll on your own schedule. Reach for the stream when you want push-based, low-latency delivery without managing a poll loop. For plain catalog browsing, list episodes (which also accepts fully_ingested=true) is simpler still.
Related
- Episodes — the same episode shape, by query or by ID
- Transcripts — dialogue available once an episode reaches
transcribed - Segments & clips — available at
segmentedandfully_ingested