The episode stream is a long-lived Server-Sent Events (SSE) connection that pushes episodes to you the moment they reach a chosen stage of ingestion — no polling. Open one connection, pick the level of hydration you care about, optionally filter to the podcasts you follow, and receive each new episode as it crosses that point.Documentation Index
Fetch the complete documentation index at: https://docs.particle.pro/llms.txt
Use this file to discover all available pages before exploring further.
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.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.milestone | Delivered when… | What’s populated on the episode |
|---|---|---|
discovered | the episode first appears in the feed | Title, URL, publish date, podcast, basic metadata. No transcript/segments/clips yet. |
transcribed (default) | speech-to-text + speaker identification finish | Full diarized transcript and identified speakers. |
segmented | the transcript is broken into structural segments | Segments (intros, ad reads, topic blocks). |
fully_ingested | ingestion is complete | Clips and the full enrichment set. This is the terminal contract: anything added to “fully ingested” in future automatically flows to subscribers of this milestone. |
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.
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.
| Parameter | Description |
|---|---|
milestone | One of discovered, transcribed, segmented, fully_ingested. Defaults to transcribed. |
podcast_ids | Slugs or IDs to filter to (union with popularity_threshold). GET: comma-separated, ≤100. POST: JSON array, ≤1000. |
popularity_threshold | Number in (0, 1). Deliver only podcasts at or above this popularity percentile. |
cursor | Opaque resume token from a previously received event. See Resuming. |
since | ISO 8601 date or date-time to backfill from when you have no cursor. Ignored if cursor is set. |
include | Heavy relations to embed in each episode (comma-separated): transcript, segments, clips, or all. Omitted by default. See Hydrate the payload. |
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:
include value | Embeds | Available at milestone |
|---|---|---|
transcript | episode.transcript — the dialogue transcript, identical to GET /v1/podcasts/episodes/{id}/transcript?format=dialogue | transcribed |
segments | episode.segments | segmented |
clips | episode.clips | fully_ingested |
all | everything available at the chosen milestone | — |
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.
If your consumer falls too far behind to keep up, the server ends the connection deliberately. This is not data loss: reconnect from your last stored cursor and the catch-up replay fills the gap. The golden rule is simply always reconnect from your last processed cursor.
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