Skip to main content

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 feed is a poll endpoint for “what’s new.” Each call returns the episodes that reached a chosen ingestion milestone since your cursor, in strict ingestion order, along with a cursor to resume from on your next poll. It’s available on every plan.
Which endpoint do I want?
  • List episodes — browse/search the catalog by podcast, entity, company, date, etc. Ordered by publish date.
  • Episode feed (this page) — poll for newly ingested episodes, resumably, on any plan.
  • Episode stream — the same data pushed in real time over SSE. Enterprise.
GET https://api.particle.pro/v1/podcasts/episodes/feed
The response shape is identical to list episodes — a data array of episodes plus has_more and cursor. The difference is the ordering and cursor: the feed is strictly monotonic in ingestion time with a keyset cursor, so polling never skips or re-counts episodes the way an offset list can when new rows arrive.

How polling works

Polling forward from now takes at least two calls: the first only mints a cursor; every call after it delivers episodes. (Backfilling with since is the exception — that first call returns a backlog straight away; see Cold starts.)
  1. First call — omit cursor and since. The page is always empty — its only job is to return a cursor marking “now” to poll forward from. You still pass your podcast filter (it’s required on every call), but it has no effect on this empty page: the cursor is a position in the global ingestion log, not a per-filter bookmark.
  2. Subsequent calls — pass the cursor from the previous response, and re-send the same filter (the cursor doesn’t carry it). You get every episode that reached your milestone since then, oldest-first, up to limit, and a new cursor.
  3. Persist the cursor and keep polling. Poll again immediately while has_more is true (you’re catching up); poll on your own interval once has_more is false (you’re caught up).
Advancing your cursor after each page gives you each episode exactly once. A small safety lag means an episode appears a few seconds after it’s ingested — the trade for never skipping one.
Once you have a cursor, resume from it — never from an episode’s timestamps. The feed is ordered by ingestion time (when an episode reached your milestone), which is unrelated to publish time — a back-catalog episode transcribed today enters the feed today, old publish date and all. The cursor is the exact high-water mark in that ingestion log. Rebuilding a start position from the episodes you consumed (their published_at or any other field) would silently skip episodes that were ingested after your last poll but published earlier — and the risk compounds the moment your filter covers more than one podcast.since is the lone exception and applies only to the first call (it’s ignored once a cursor is set): a backfill anchor, also measured in ingestion time. See Cold starts for the right value to give it.

Milestones and filters

Pick one milestone (default transcribed) — episodes are returned once they reach that stage. The milestones work as they do for the stream, but the feed caps the podcast set it covers (see below):
ParameterDescription
milestonediscovered, transcribed (default), segmented, or fully_ingested.
podcast_idsSlugs or IDs to filter to (comma-separated). Hard limit of 100 per request. Unknown values are ignored; if no filter matches any podcast, the call returns 404.
popularity_thresholdNumber in (0,1); covers the most-popular podcasts at/above this popularity percentile.
topic_idsOne or more topics (comma-separated): a slug path (e.g. sports/football/fantasy-football), ID, or ancestry hash. A topic also matches its descendants, and multiple topics are unioned. Scopes the covered podcasts to those that regularly cover the topic. Hard limit of 20 per request. Unknown topic returns 404.
cursorHigh-water mark from a prior response — the exact ingestion-log position to resume from. Omit (with no since) to start from now.
sinceFirst-call-only backfill anchor (ISO 8601); used only when no cursor is supplied, ignored once one is. Measured in ingestion time (when episodes entered the feed), not publish time — see Cold starts.
limitMax episodes per page (1–100, default 25).
includeHeavy relations to embed in each episode (comma-separated): transcript, segments, clips, or all. Omitted by default. See Hydrate the payload.

The feed covers at most 100 podcasts

The feed is the bounded, all-plans poll — it never fans out to an unbounded podcast set. You must supply podcast_ids, topic_ids, popularity_threshold, or a combination (a request with none returns 422); the feed can’t be polled across every podcast. Whatever you supply, the feed covers at most 100 podcasts:
  • Your podcast_ids are always included (capped at 100 per request).
  • popularity_threshold then fills any remaining slots with the most-popular podcasts above the threshold, dropping the least-popular tail to stay within 100. A low threshold can qualify many times more than 100 podcasts; the feed trims that to the top 100 by popularity — it doesn’t reject the request.
  • topic_ids scopes that fill to one or more topics: the slots go to the most-popular podcasts that regularly cover the topic — a show that merely mentions it in passing (one fantasy-football episode out of hundreds of interviews) is excluded, while shows that genuinely focus on it are kept. Use it alone to follow the top shows in a niche, or with popularity_threshold to also apply a global popularity floor. The popularity threshold stays a global percentile, so for a niche topic prefer topic_ids on its own.
If you need real-time delivery across an unbounded podcast set (e.g. every podcast above a popularity floor), use the stream (Enterprise) — that’s what it’s for.
Prefer an explicit, stable set? The feed’s topic_ids and popularity_threshold are dynamic — each poll re-derives the covered podcasts (the most-popular shows in the topic), so the set drifts as charts and topic coverage move. To pin a fixed set instead, curate it once with the catalog endpoint and pass the result as podcast_ids:
GET /v1/podcasts?topic_id=sports/football/fantasy-football&popularity_threshold=0.75
That returns the podcasts where the topic carries a meaningful share of episodes and that clear a global popularity percentile. Take their IDs, pass them to the feed as podcast_ids, and you’ll poll exactly that list — explicit and stable — rather than whatever currently tops the charts. As on the feed, popularity_threshold there is global (a percentile across all charting podcasts, not within the topic), so widen or drop it if a narrow topic returns too few podcasts.

Hydrate the payload

Each episode carries only its metadata, counts, and flags (has_transcript, segment_count, clip_count) by default — the heavy relations are not shipped. To embed them and avoid a follow-up request per episode, pass include:
include valueEmbedsAvailable at milestone
transcriptepisode.transcript — the dialogue transcript, identical to GET /v1/podcasts/episodes/{id}/transcript?format=dialoguetranscribed
segmentsepisode.segmentssegmented
clipsepisode.clipsfully_ingested
alleverything available at the chosen milestone
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 returns 422. 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). Word-level transcripts are paginated and can’t be embedded inline; fetch them from GET /v1/podcasts/episodes/{id}/transcript/words.

Example: follow five podcasts

Say you want a row in your store every time Acquired, All-In, No Priors, My First Million, or The Tim Ferriss Show drops a new episode. Pass them as podcast_ids on every call.
PODS="acquired,all-in,no-priors,my-first-million,the-tim-ferriss-show"

# 1. First call — no cursor. The page is always empty; you're here for the cursor.
curl "https://api.particle.pro/v1/podcasts/episodes/feed?milestone=transcribed&podcast_ids=$PODS" \
  -H "X-API-Key: $PARTICLE_API_KEY"
# -> { "data": [], "has_more": false, "cursor": "g3Qk9m8..." }

# 2. Poll with that cursor (re-send the same podcast_ids). Nothing new yet, so
#    still empty — you've confirmed the loop and the cursor holds at "now".
curl "https://api.particle.pro/v1/podcasts/episodes/feed?milestone=transcribed&podcast_ids=$PODS&cursor=g3Qk9m8..." \
  -H "X-API-Key: $PARTICLE_API_KEY"
# -> { "data": [], "has_more": false, "cursor": "g3Qk9m8..." }

# 3. Hours later, poll again with your saved cursor. Acquired and No Priors each
#    shipped an episode meanwhile, so they return oldest-first with a fresh cursor.
curl "https://api.particle.pro/v1/podcasts/episodes/feed?milestone=transcribed&podcast_ids=$PODS&cursor=g3Qk9m8..." \
  -H "X-API-Key: $PARTICLE_API_KEY"
# -> { "data": [ {…acquired…}, {…no-priors…} ], "has_more": false, "cursor": "k2Wn8x1..." }
Want the most-popular podcasts rather than a named set? Swap podcast_ids for popularity_threshold (e.g. 0.9) — everything else is identical. Want the top shows in a topic — say fantasy football? Use topic_ids=sports/football/fantasy-football (alone, or with popularity_threshold); you poll up to the 100 most-popular shows on that topic without curating a list, and new shows that rise into it are picked up automatically.

Feed vs. stream

The feed and the stream deliver the same episodes at the same milestones with the same filters — the feed is the pull form, the stream the push form. The key difference: the feed always requires a filter and bounds its coverage to 100 podcasts, while the stream (Enterprise) can fan out to an unbounded set above a popularity floor. Use the feed when polling is simpler for you or you’re not on Enterprise; use the stream for real-time, low-latency delivery without managing a poll loop. Cursors are not interchangeable between them.

Cold starts: existing data or a lost cursor

Most callers don’t begin from a blank slate — you already hold some episodes, or you lost the cursor you were persisting. The cursor is the only exact resume point, so once you have one, keep it. To (re)start without one, pick one of two clean paths:
  • Start at “now” and move on. Make a cursorless call for a fresh cursor and poll forward. You’ll never miss a future episode; you just won’t replay the window you were away. Reconcile that window, if you need it, with list episodes — it’s built for publish-ordered catalog backfill, which the feed’s since is not.
  • Replay a window with since. Set since to a wall-clock lower bound — the time of your last good sync, or “now minus the longest you might have been offline.” Because since is ingestion time, since=<7 days ago> means everything that entered the feed in the last 7 days. Unlike the cursorless call, this first call returns that backlog right away (page through has_more), and you resume from the cursor it returns. Idempotent writes absorb any overlap with episodes you already have.
PODS="acquired,all-in,no-priors,my-first-million,the-tim-ferriss-show"

# Lost the cursor — replay everything ingested since your last good sync,
# then keep polling with the cursor this returns:
curl "https://api.particle.pro/v1/podcasts/episodes/feed?milestone=transcribed&podcast_ids=$PODS&since=2026-05-01T12:00:00Z" \
  -H "X-API-Key: $PARTICLE_API_KEY"
Prefer a wall-clock value over an episode’s publish date. It’s tempting to take the newest published_at you already hold and pass it as since. It usually works as a no-gap lower bound — an episode is ingested at or after it’s published, so anything newer lands beyond your mark — but it has two rough edges: it re-delivers back-catalog episodes that were published earlier yet ingested after your mark, and it breaks outright if any feed carries a future publish date (some do), which pushes since ahead of real ingestion times and skips episodes. A wall-clock sync time has neither problem, so reach for it first. If you’ve lost the cursor and have no record of when you last synced, the newest published_at you hold is a reasonable last resort — it still won’t skip future episodes (barring a feed that dates an episode in the future), but you will get back episodes you already have, so make your writes idempotent and let the overlap fall away.