# Get a company Source: https://docs.particle.pro/api-reference/companies/get-a-company /openapi.json get /v1/companies/{id} Returns a single company by slug (e.g., 'apple'), domain (e.g., 'apple.com'), or ID. # Get company advertising profile Source: https://docs.particle.pro/api-reference/companies/get-company-advertising-profile /openapi.json get /v1/companies/{id}/podcast/advertising Returns advertising intelligence for a specific company across the podcast ecosystem, including reach metrics and recent ad placements. Identify the company by slug (e.g., 'apple'), domain, or ID. # List companies Source: https://docs.particle.pro/api-reference/companies/list-companies /openapi.json get /v1/companies Returns a paginated list of companies. Filter by entity slug (e.g., 'apple'), ticker, domain, CIK, or QID. Search by name or fetch updates since a timestamp. # List company competitors Source: https://docs.particle.pro/api-reference/companies/list-company-competitors /openapi.json get /v1/companies/{id}/competitors Returns a paginated list of competitors for a company, ordered by prominence (news coverage volume, market cap, podcast appearances, Wikidata notability) so the largest / most-newsworthy competitors come first. Each result includes a competitive basis describing the relationship. Identify the company by slug (e.g., 'apple'), domain, or ID. # List company people Source: https://docs.particle.pro/api-reference/companies/list-company-people /openapi.json get /v1/companies/{id}/people Returns a paginated list of people associated with a company (executives and other known roles), current roles first and most-recently-joined first. Each person carries the full Person payload — bio, role history, external profile links, and knowledge-graph cross-reference. Set current_only=false to include historical roles, or filter by role title. Identify the company by slug (e.g., 'apple'), domain, or ID. # List company products Source: https://docs.particle.pro/api-reference/companies/list-company-products /openapi.json get /v1/companies/{id}/products Returns the product hierarchy for a company as a nested tree. Segments contain product lines, which contain individual products. Identify the company by slug (e.g., 'apple'), domain, or ID. Defaults to active products only. # Get clip embed Source: https://docs.particle.pro/api-reference/embed/get-clip-embed /openapi.json get /v1/embed/clips/{id} Returns a minimal public representation of a clip for embed rendering. # Get clip embed transcript Source: https://docs.particle.pro/api-reference/embed/get-clip-embed-transcript /openapi.json get /v1/embed/clips/{id}/transcript Returns the diarized transcript for a clip embed. # Get episode embed code Source: https://docs.particle.pro/api-reference/embed/get-episode-embed-code /openapi.json get /v1/embed/episodes/{episode_id}/code Returns a paste-ready HTML snippet for an existing clip or a custom timestamp range within an episode. Either clip_id or start (with optional end) must be supplied; the two modes are mutually exclusive. For slices: when end is omitted, it defaults to start + 120 seconds. The embed widget caps slice length at 120 seconds, so any end value beyond start + 120s is silently clamped to start + 120s. End is also clamped to the episode duration. The actual range used is encoded in the returned script_url. # Get an entity Source: https://docs.particle.pro/api-reference/entities/get-an-entity /openapi.json get /v1/entities/{id} Returns a single knowledge graph entity by slug (e.g., 'sam-altman', 'apple') or ID. # List entities Source: https://docs.particle.pro/api-reference/entities/list-entities /openapi.json get /v1/entities Search and list knowledge graph entities (people, organizations, places) across all podcast content. Defaults to the most frequently appearing entities ranked by number of distinct podcast episodes featuring them when no query or podcast filter is provided. Filter by podcast slug or ID. # List entity types Source: https://docs.particle.pro/api-reference/entities/list-entity-types /openapi.json get /v1/entities/types Returns the entity categories supported as values for the `type` query parameter on /v1/entities and the `entity_type` query parameter on /v1/podcasts/search. # Introduction Source: https://docs.particle.pro/api-reference/introduction Particle API reference — base URL, authentication, and machine-readable specs. ## Base URL All API endpoints are served from: ``` https://api.particle.pro ``` ## Authentication Every endpoint that returns user data requires an API key. Two header forms are accepted; `X-API-Key` is recommended. ### `X-API-Key` header (recommended) ```bash theme={"dark"} curl https://api.particle.pro/v1/podcasts \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ### `Authorization: Bearer` header Equivalent in every way; useful when you're routing through middleware that already understands bearer tokens. ```bash theme={"dark"} curl https://api.particle.pro/v1/podcasts \ -H "Authorization: Bearer $PARTICLE_API_KEY" ``` The `/v1/embed/*` endpoints are public and intentionally do not require authentication. ## Get an API key 1. Sign up or log in at the API Dashboard 2. Create an organization and project 3. Open the project's **API Keys** section 4. Click **Create API Key** and copy the key — it won't be shown again ## Machine-readable resources For automated integrations and AI-assisted development: | Resource | URL | Use it for | | ------------------- | ----------------------------------------------------------- | --------------------------------------------------------- | | Public OpenAPI spec | [`/openapi.json`](/openapi.json) | Code generation, schema validation, contract testing | | `llms.txt` | [`/llms.txt`](https://docs.particle.pro/llms.txt) | Compact index of every doc page for LLM context | | `llms-full.txt` | [`/llms-full.txt`](https://docs.particle.pro/llms-full.txt) | Full-text concatenation of every guide for deep retrieval | The OpenAPI spec is auto-generated from the server source on every release and the page-level reference that follows is rendered directly from it. It is the authoritative reference for endpoint structure; for individual field semantics, defer to the live API response when the two disagree (occasional drift can happen between schema annotations and serialized values). ## Conventions See [Concepts](/concepts) for the rules every endpoint follows: ID/slug resolution, cursor pagination, error envelope, and pricing weight. # Get a Person Source: https://docs.particle.pro/api-reference/people/get-a-person /openapi.json get /v1/people/{id} Returns the comprehensive Person payload including display name, slug, headshot, curated bio, the Person's company-role history (most recent first), known third-party profile links, and an optional knowledge-graph cross-reference. The {id} parameter accepts the Person slug (recommended) or the encoded Person UUID. # List a Person's external profiles Source: https://docs.particle.pro/api-reference/people/list-a-persons-external-profiles /openapi.json get /v1/people/{slug}/external-links Returns every known third-party profile (LinkedIn, Wikipedia, social) for the Person. Discovery happens asynchronously via the v1.person.created enrichment subscriber; a freshly-created Person may not yet have all its profiles populated. # Get a sponsor Source: https://docs.particle.pro/api-reference/podcast-advertising/get-a-sponsor /openapi.json get /v1/podcasts/advertising/sponsors/{id} Returns a single advertising sponsor with activity metrics. The {id} path parameter accepts a sponsor ID, a company ID, a company domain, or a company slug. # Get advertising leaderboard Source: https://docs.particle.pro/api-reference/podcast-advertising/get-advertising-leaderboard /openapi.json get /v1/podcasts/advertising/leaderboard Returns the top podcast advertising sponsors ranked by the specified metric, with optional time range and company filtering. # Get sponsor co-occurrence Source: https://docs.particle.pro/api-reference/podcast-advertising/get-sponsor-co-occurrence /openapi.json get /v1/podcasts/advertising/co-occurrence Returns pairs of sponsors that frequently appear together in the same podcast episodes. # List podcasts for a sponsor Source: https://docs.particle.pro/api-reference/podcast-advertising/list-podcasts-for-a-sponsor /openapi.json get /v1/podcasts/advertising/sponsors/{id}/podcasts Returns a paginated list of podcasts where the sponsor advertises, ordered by the number of episodes featuring the sponsor. # List sponsors Source: https://docs.particle.pro/api-reference/podcast-advertising/list-sponsors /openapi.json get /v1/podcasts/advertising/sponsors Returns a paginated list of podcast advertising sponsors, optionally filtered by name search or company. # Get publisher political bias profile Source: https://docs.particle.pro/api-reference/podcast-bias/get-publisher-political-bias-profile /openapi.json get /v1/podcasts/publishers/{id}/bias Returns political bias intelligence rolled up across every analyzed podcast attributed to the publisher: coverage stats, political share, average lean (on a -3..+3 ordinal scale), lean diversity, the bias-bucket and regional distributions, and the most recent evaluation timestamp. Score-based metrics are omitted when the publisher has no political podcasts. # List analyzed podcasts for a publisher Source: https://docs.particle.pro/api-reference/podcast-bias/list-analyzed-podcasts-for-a-publisher /openapi.json get /v1/podcasts/publishers/{id}/bias/podcasts Returns a paginated list of the publisher's analyzed podcasts with their latest bias analysis attached. Supports filtering by bias bucket, political_context, and a toggle for excluding NOT_POLITICAL podcasts, plus sort by lean_score, evaluated_at, or name. # List publishers with podcasts in a bias bucket Source: https://docs.particle.pro/api-reference/podcast-bias/list-publishers-with-podcasts-in-a-bias-bucket /openapi.json get /v1/podcasts/bias/{result}/publishers Returns a paginated list of publishers whose analyzed catalog contains at least min_count podcasts in the requested bias bucket, ranked by raw count or by share of the publisher's analyzed catalog. Useful as a flip of the publisher bias profile — answers 'which publishers carry the most RIGHT-leaning shows?'. # Rank publishers by political bias metric Source: https://docs.particle.pro/api-reference/podcast-bias/rank-publishers-by-political-bias-metric /openapi.json get /v1/podcasts/bias/publishers/leaderboard Returns a paginated leaderboard ranking publishers by a chosen bias metric: most_left_leaning, most_right_leaning, most_political, most_diverse, most_monolithic, or most_analyzed. min_analyzed_podcasts and min_political_podcasts gate small-sample publishers from the ranking. Optional political_context, since, and until restrict the corpus before aggregation. # Get a cross-publisher suitability leaderboard Source: https://docs.particle.pro/api-reference/podcast-brand-suitability/get-a-cross-publisher-suitability-leaderboard /openapi.json get /v1/podcasts/suitability/publishers/leaderboard Returns a paginated, ranked list of podcast publishers by IAB/GARM brand-suitability composition. Pick a metric (safest, riskiest, most_placeable, most_analyzed) to choose how the ranking is constructed. min_analyzed_podcasts guards against tiny catalogs dominating share-based metrics. The ranking reflects each publisher's current catalog composition; paginate the audit history per show via GET /v1/podcasts/{id}/suitability?include=history when you need point-in-time analysis. Each row carries the full tier_breakdown (counts + shares) plus the metric_value the row was ranked by. # Get publishers ranked by exposure to a brand-safety category Source: https://docs.particle.pro/api-reference/podcast-brand-suitability/get-publishers-ranked-by-exposure-to-a-brand-safety-category /openapi.json get /v1/podcasts/suitability/categories/{code}/publishers For one of the 12 GARM brand-safety categories, returns a paginated ranking of podcast publishers by exposure across their catalog. The killer view for regulated brands: an alcohol advertiser pivots on illegal_drugs_alcohol_tobacco and immediately sees which publishers carry the highest catalog-level exposure (and at what prevalence + treatment); a family brand pivots on adult_sexual or hate_speech_aggression to build an exclusion list. min_prevalence + treatment narrow what counts as 'exposed'. Each row includes prevalence + treatment breakdowns and up to three example podcasts in the publisher's catalog that exhibit the highest exposure. # Get a clip Source: https://docs.particle.pro/api-reference/podcast-clips/get-a-clip /openapi.json get /v1/podcasts/clips/{id} Returns a single clip by its unique identifier. # List clips Source: https://docs.particle.pro/api-reference/podcast-clips/list-clips /openapi.json get /v1/podcasts/clips Returns AI-extracted highlight clips across podcast episodes, ranked by engagement potential. For text-based clip discovery (semantic, keyword, or entity mention search), use GET /v1/podcasts/search — matching clips are embedded inside each search result alongside the parent segment and dialogue. # List clips for an episode Source: https://docs.particle.pro/api-reference/podcast-clips/list-clips-for-an-episode /openapi.json get /v1/podcasts/episodes/{id}/clips Returns the AI-extracted highlight clips for a specific episode, sorted by engagement score. # Search podcast dialogue for entity mentions Source: https://docs.particle.pro/api-reference/podcast-episode-search/search-podcast-dialogue-for-entity-mentions /openapi.json get /v1/podcasts/mentions Returns every line of dialogue that mentions a person or company, grouped by episode and ordered by recency. Each result is one episode plus all of that episode's mention windows — a window is a contiguous range of dialogue containing the mention with `context_lines` of surrounding context, and lines containing the mention have `is_mention=true`. Use this endpoint for the "every line about X" use case. For finding dialogue by topic or exact phrase use `/v1/podcasts/search` instead. # Search podcast episode content Source: https://docs.particle.pro/api-reference/podcast-episode-search/search-podcast-episode-content /openapi.json get /v1/podcasts/episodes/search To find podcasts (shows) by name or topic, use `/v1/podcasts?q=…`. This endpoint searches *inside* episode dialogue — by meaning (`semantic_search`), by exact phrase (`keyword_search`), or both at once (hybrid). Each result is a segment of an episode, returned with bounded preview windows of dialogue and any highlight clips that overlap the segment. For "every line about a person or company" use `/v1/podcasts/mentions` instead — that endpoint returns episode-grouped mention windows with line-level highlights and is shaped for the read-everything-about-X use case. `entity_id` and `company_id` here narrow the result set to episodes featuring the resolved entity, but the ranking still comes from `semantic_search` / `keyword_search`. # Search podcast episode content Source: https://docs.particle.pro/api-reference/podcast-episode-search/search-podcast-episode-content-1 /openapi.json get /v1/podcasts/search This endpoint is moving to `/v1/podcasts/episodes/search` — prefer that path for new integrations. To find podcasts (shows) by name or topic, use `/v1/podcasts?q=…`. This endpoint searches *inside* episode dialogue — by meaning (`semantic_search`), by exact phrase (`keyword_search`), or both at once (hybrid). Each result is a segment of an episode, returned with bounded preview windows of dialogue and any highlight clips that overlap the segment. For "every line about a person or company" use `/v1/podcasts/mentions` instead — that endpoint returns episode-grouped mention windows with line-level highlights and is shaped for the read-everything-about-X use case. `entity_id` and `company_id` here narrow the result set to episodes featuring the resolved entity, but the ranking still comes from `semantic_search` / `keyword_search`. # Get an episode Source: https://docs.particle.pro/api-reference/podcast-episodes/get-an-episode /openapi.json get /v1/podcasts/episodes/{id} Returns a single episode by its unique identifier. # List ads in an episode Source: https://docs.particle.pro/api-reference/podcast-episodes/list-ads-in-an-episode /openapi.json get /v1/podcasts/episodes/{id}/ads Returns the advertising spots detected in a specific episode, including the sponsor, the linked company in the knowledge graph, the advertised product or offer, the read type (HOST_READ vs PRE_RECORDED), and the placement (PRE_ROLL/MID_ROLL/POST_ROLL). Network promos are excluded. # List entities in an episode Source: https://docs.particle.pro/api-reference/podcast-episodes/list-entities-in-an-episode /openapi.json get /v1/podcasts/episodes/{id}/entities Returns knowledge graph entities mentioned in a specific episode, with salience scores and occurrence counts. # List episodes Source: https://docs.particle.pro/api-reference/podcast-episodes/list-episodes /openapi.json get /v1/podcasts/episodes Returns a paginated list of podcast episodes across all podcasts. Filter by podcast slug or ID, entity slug or ID, company slug or domain, date range, and language. # List speakers in an episode Source: https://docs.particle.pro/api-reference/podcast-episodes/list-speakers-in-an-episode /openapi.json get /v1/podcasts/episodes/{id}/speakers Returns the identified speakers in a specific episode with their roles and speaking durations. # List topics for an episode Source: https://docs.particle.pro/api-reference/podcast-episodes/list-topics-for-an-episode /openapi.json get /v1/podcasts/episodes/{id}/topics Returns the topic taxonomy classifications for a specific episode. # Poll the episode feed Source: https://docs.particle.pro/api-reference/podcast-episodes/poll-the-episode-feed /openapi.json get /v1/podcasts/episodes/feed Resumable, strictly-ordered poll of episodes as they are ingested — the all-plans pull alternative to the Enterprise stream. Subscribe to a milestone (default transcribed); each call returns episodes that reached it since your cursor, in ingestion order, with a cursor to poll again. Optionally filter by podcast_ids, topic_ids, and/or popularity_threshold. # Get a guest's brand-suitability exposure profile Source: https://docs.particle.pro/api-reference/podcast-guests/get-a-guests-brand-suitability-exposure-profile /openapi.json get /v1/podcasts/guests/{id}/suitability Returns the distribution of IAB Tech Lab brand-suitability tiers across the podcasts a guest has appeared on, plus the most-frequently flagged categories observed in those shows. This measures the guest's exposure across the shows they've appeared on — not a verdict on the guest themselves. # Get a podcast guest profile Source: https://docs.particle.pro/api-reference/podcast-guests/get-a-podcast-guest-profile /openapi.json get /v1/podcasts/guests/{id} Returns the lifetime profile for a podcast guest including their Person identity (name, slug, headshot, current title/company), total appearances, distinct podcasts, role mix, and the most-frequent shows they've appeared on. The {id} parameter accepts either the Person slug (recommended) or the encoded Person UUID. # List appearances for a podcast guest Source: https://docs.particle.pro/api-reference/podcast-guests/list-appearances-for-a-podcast-guest /openapi.json get /v1/podcasts/guests/{id}/appearances Returns the chronological list of episodes a guest has appeared on, most recent first. Supports filtering by podcast, role, topic, date range, brand-safety tier, and minimum speaking duration. # List podcast guests Source: https://docs.particle.pro/api-reference/podcast-guests/list-podcast-guests /openapi.json get /v1/podcasts/guests Returns a paginated directory of Persons who have appeared on at least one podcast episode in a non-host listing role. Filter by name, topic, podcast, date, and brand-safety tier. # List podcasts a guest has appeared on Source: https://docs.particle.pro/api-reference/podcast-guests/list-podcasts-a-guest-has-appeared-on /openapi.json get /v1/podcasts/guests/{id}/podcasts Returns the distinct set of podcasts a guest has appeared on, with per-podcast appearance counts, first/last appearance dates, and the podcast's brand-safety and bias attributes when evaluated. # List the guest roster for a podcast Source: https://docs.particle.pro/api-reference/podcast-guests/list-the-guest-roster-for-a-podcast /openapi.json get /v1/podcasts/{id}/guests Returns the paginated set of identified guests who have appeared on the podcast, grouped by Person. Each row carries the Person identity, the per-podcast appearance count, first/last appearance dates, and dominant listing role. # List trending podcast guests Source: https://docs.particle.pro/api-reference/podcast-guests/list-trending-podcast-guests /openapi.json get /v1/podcasts/guests/trends Surfaces guests who are currently making the podcast rounds — the kind of cross-show interview activity that signals a book launch, product unveil, news-cycle moment, or new-on-the-scene debut. Each row is a guest whose recent activity is materially elevated above their own historical baseline AND who has appeared on multiple distinct podcasts inside the window with substantive (5+ minute) interviews. Filter by recency window, topic, brand-safety tier, and an optional first-appearance cutoff to find people new to the scene. This is not a leaderboard of perennial podcast regulars, recurring co-hosts, or daily news-segment contributors — those are filtered out by design; use /v1/podcasts/guests for a steady-state directory. # Get a podcast publisher Source: https://docs.particle.pro/api-reference/podcast-publishers/get-a-podcast-publisher /openapi.json get /v1/podcasts/publishers/{id} Returns a single podcast publisher by slug (e.g., 'goalhanger', 'iheartpodcasts', 'bbc-radio-4') or ID. Slugs are human-readable identifiers populated for the vast majority of publishers and are the recommended way to reference a publisher in URLs. The ID always works as a fallback for publishers whose name doesn't slugify. # Get a publisher's brand suitability profile Source: https://docs.particle.pro/api-reference/podcast-publishers/get-a-publishers-brand-suitability-profile /openapi.json get /v1/podcasts/publishers/{id}/suitability Returns a publisher-level rollup of IAB Tech Lab Brand Safety & Suitability Framework (formerly GARM) verdicts across the publisher's catalog: tier composition (SAFE / LIMITED / SENSITIVE / UNSAFE), confidence distribution, per-category exposure across all 12 GARM dimensions, and the top concerns surfaced from the category rollup. The coverage block (analyzed_coverage as a 0..1 share + a LOW/MEDIUM/HIGH quality enum) tells callers how representative the rollup is — analyses are still being backfilled across the long tail of the catalog. Identify the publisher by slug (e.g., 'iheartpodcasts', 'bbc-radio-4', 'bloomberg') or ID. # List a publisher's podcasts with their suitability verdicts Source: https://docs.particle.pro/api-reference/podcast-publishers/list-a-publishers-podcasts-with-their-suitability-verdicts /openapi.json get /v1/podcasts/publishers/{id}/suitability/podcasts Returns a paginated list of the publisher's analyzed podcasts decorated with their latest IAB/GARM tier, confidence, evaluated-at timestamp, and any non-NONE category exposures (flagged categories). Filter by tier(s), by a specific GARM category (optionally constrained to a minimum prevalence and a particular treatment), and choose how to sort (most-risky first by default). Per-category reasoning and evidence excerpts are intentionally omitted — fetch GET /v1/podcasts/{id}/suitability for full per-category detail. # List podcast publishers Source: https://docs.particle.pro/api-reference/podcast-publishers/list-podcast-publishers /openapi.json get /v1/podcasts/publishers Returns a paginated list of podcast publishers, ordered by catalog size (largest first, the default) or alphabetically by name. Each entry includes the publisher name, slug, and the number of podcasts attributed to the publisher. # List podcasts for a publisher Source: https://docs.particle.pro/api-reference/podcast-publishers/list-podcasts-for-a-publisher /openapi.json get /v1/podcasts/publishers/{id}/podcasts Returns a paginated list of podcasts attributed to a publisher, ordered by popularity. Identify the publisher by slug (e.g., 'goalhanger', 'iheartpodcasts', 'bbc-radio-4') or ID. # Get chart slot history Source: https://docs.particle.pro/api-reference/podcast-rankings/get-chart-slot-history /openapi.json get /v1/podcasts/rankings/history Returns historical chart snapshots for a chart slot, ordered most-recent first. Set `since` / `until` to bound the time range; set `podcast_id` to filter to one podcast within the slot. # Get historical rankings for a podcast Source: https://docs.particle.pro/api-reference/podcast-rankings/get-historical-rankings-for-a-podcast /openapi.json get /v1/podcasts/{id}/rankings/history Returns the historical rank entries for a podcast across one or more chart slots. Optional filters narrow the scope (single source, single country, single category, or a date range). # List current rankings for a podcast Source: https://docs.particle.pro/api-reference/podcast-rankings/list-current-rankings-for-a-podcast /openapi.json get /v1/podcasts/{id}/rankings Returns every live chart appearance of the given podcast — across sources, countries, and categories. Identify the podcast by slug (e.g., 'the-daily') or ID. # List podcast rankings Source: https://docs.particle.pro/api-reference/podcast-rankings/list-podcast-rankings /openapi.json get /v1/podcasts/rankings Returns chart entries from the live ranking snapshot. The most common call needs no parameters — it returns the first page (default 25, max 100 per request) of the US Apple Top Podcasts overall chart, ordered by rank ascending. Charts run to rank 200; paginate with `cursor` to fetch the rest. Narrow the result with `country`, `category_slug`, `source`, or `podcast_id`. When `podcast_id` is set, the response is the podcast's current chart appearances across the matching slots. # List ranking categories Source: https://docs.particle.pro/api-reference/podcast-rankings/list-ranking-categories /openapi.json get /v1/podcasts/rankings/categories Returns every category currently represented in the rankings dataset, optionally restricted to a single source. For Apple sub-categories the response includes `parent_slug` so clients can render the category hierarchy. # List ranking countries Source: https://docs.particle.pro/api-reference/podcast-rankings/list-ranking-countries /openapi.json get /v1/podcasts/rankings/countries Returns every country currently represented in the rankings dataset, optionally restricted to a single source. Each entry carries the human-readable country name (when known) and a count of distinct chart slots. # List ranking movers Source: https://docs.particle.pro/api-reference/podcast-rankings/list-ranking-movers /openapi.json get /v1/podcasts/rankings/movers Returns the chart entries whose rank changed between the live snapshot and the comparison snapshot `window_days` ago. Use the `change` filter to focus on debuts (`new`), departures (`exit`), or directional moves (`up` / `down`). Stable rows are excluded. # List ranking sources Source: https://docs.particle.pro/api-reference/podcast-rankings/list-ranking-sources /openapi.json get /v1/podcasts/rankings/sources Returns each (source, chart_type) pair available on this API together with row counts and freshness for the live snapshot. # Summarize a podcast's chart presence Source: https://docs.particle.pro/api-reference/podcast-rankings/summarize-a-podcasts-chart-presence /openapi.json get /v1/podcasts/{id}/rankings/summary Returns a one-call aggregate of the podcast's current chart presence: the number of distinct chart slots, sources, countries, and categories it appears on, plus the single best (lowest-numbered) rank it currently holds and a per-source breakdown. # List ratings for a podcast Source: https://docs.particle.pro/api-reference/podcast-ratings/list-ratings-for-a-podcast /openapi.json get /v1/podcasts/{id}/ratings Returns user-generated ratings (1-5 stars plus optional review text) captured from third-party platforms. Today's source is Apple Podcasts, walked across the Anglophone storefronts (us, gb, ca, au, nz, ie). Newest ratings first; paginate with `cursor`. Filter by `platform_slug`, `locale`, `min_stars`, or a date window on `since`/`until`. # Summarize a podcast's ratings Source: https://docs.particle.pro/api-reference/podcast-ratings/summarize-a-podcasts-ratings /openapi.json get /v1/podcasts/{id}/ratings/summary Returns the numeric aggregate of a podcast's ratings — one entry per (platform, locale) it has been rated on, a combined cross-source roll-up, and the latest LLM-generated narrative summary of recent review text when one has been generated. # Get a segment Source: https://docs.particle.pro/api-reference/podcast-segments/get-a-segment /openapi.json get /v1/podcasts/segments/{id} Returns a single segment by its unique identifier. # List segments Source: https://docs.particle.pro/api-reference/podcast-segments/list-segments /openapi.json get /v1/podcasts/segments Returns AI-identified segments across podcast episodes. Segments represent structural sections like topic discussions, interviews, ads, etc. At least one of `episode_id`, `podcast_id`, or `type` is required — this endpoint does not return a global feed. # List segments for an episode Source: https://docs.particle.pro/api-reference/podcast-segments/list-segments-for-an-episode /openapi.json get /v1/podcasts/episodes/{id}/segments Returns the AI-identified segments for a specific episode in chronological order. # Get clip transcript Source: https://docs.particle.pro/api-reference/podcast-transcripts/get-clip-transcript /openapi.json get /v1/podcasts/clips/{id}/transcript Returns the diarized transcript for a specific clip. # Get entity mentions in transcript Source: https://docs.particle.pro/api-reference/podcast-transcripts/get-entity-mentions-in-transcript /openapi.json get /v1/podcasts/episodes/{id}/transcript/mentions Finds all mentions of a specific entity within an episode's transcript and returns each mention with surrounding dialogue context. Useful for seeing exactly where and how an entity is discussed. # Get episode transcript Source: https://docs.particle.pro/api-reference/podcast-transcripts/get-episode-transcript /openapi.json get /v1/podcasts/episodes/{id}/transcript Returns the diarized transcript for a podcast episode. Supports dialogue (speaker-attributed lines), plain text, and SRT subtitle formats. Optionally filter by speaker or time range. # Get segment transcript Source: https://docs.particle.pro/api-reference/podcast-transcripts/get-segment-transcript /openapi.json get /v1/podcasts/segments/{id}/transcript Returns the diarized transcript for a specific segment. # Get word-level transcript Source: https://docs.particle.pro/api-reference/podcast-transcripts/get-word-level-transcript /openapi.json get /v1/podcasts/episodes/{id}/transcript/words Returns the word-level timestamped transcript for a podcast episode. Use start/end to extract a time range, limit/cursor to paginate (defaults to all words), and exclude_spacing=true to drop the inter-word whitespace tokens that NLP/LLM consumers don't need. # Get a podcast Source: https://docs.particle.pro/api-reference/podcasts/get-a-podcast /openapi.json get /v1/podcasts/{id} Returns a single podcast by slug (e.g., 'all-in') or ID. Slugs are human-readable identifiers included in every podcast response. # Get a podcast's format profile Source: https://docs.particle.pro/api-reference/podcasts/get-a-podcasts-format-profile /openapi.json get /v1/podcasts/{id}/format Returns the full format breakdown for a podcast: how often episodes feature guests, detected production formats (interview, panel, call_in, solo_narrated), advertiser and video presence, episode length distribution, publishing cadence, the publish-day histogram, and the sample sizes behind each attribute. The compact form of this data is embedded as the `format` field on podcast responses; use this endpoint for the exact rates and distributions. Returns 404 when the podcast is not found or its format profile has not been computed yet. # Get a podcast's latest bias analysis Source: https://docs.particle.pro/api-reference/podcasts/get-a-podcasts-latest-bias-analysis /openapi.json get /v1/podcasts/{id}/bias Returns the most recent automated political bias analysis for a podcast, including the agent's reasoning, transcript evidence, web research evidence, and the sample episodes that informed the rating. Returns 404 when the podcast is not found or when it has not yet been analyzed. # Get a podcast's latest brand suitability assessment Source: https://docs.particle.pro/api-reference/podcasts/get-a-podcasts-latest-brand-suitability-assessment /openapi.json get /v1/podcasts/{id}/suitability Returns the most recent brand suitability assessment for a podcast against the IAB Tech Lab Content Taxonomy 3.x Brand Safety & Suitability Framework (the industry-standard 12-category taxonomy formerly stewarded by GARM): overall tier, per-category prevalence and treatment, evidence excerpts from sampled episodes, and the methodology that produced the rating. Pass include=trend for a deterministically-derived comparison against the prior assessment, or include=history for the list of prior assessments. Returns 404 when the podcast is not found or has not yet been analyzed. # Get podcast advertising profile Source: https://docs.particle.pro/api-reference/podcasts/get-podcast-advertising-profile /openapi.json get /v1/podcasts/{id}/advertising Returns advertising intelligence for a specific podcast, including aggregate stats, read type breakdown, and top sponsors. Identify the podcast by slug (e.g., 'all-in') or ID. # Get podcast catalog statistics Source: https://docs.particle.pro/api-reference/podcasts/get-podcast-catalog-statistics /openapi.json get /v1/podcasts/stats Returns live, catalog-wide statistics: how many podcasts the catalog covers, broken out by classification, and how many episodes were added within recent trailing windows. The figures update continuously as new podcasts and episodes are ingested, so they can drive real-time counters. This endpoint is free and requires no API key. # List a podcast's third-party platform presences Source: https://docs.particle.pro/api-reference/podcasts/list-a-podcasts-third-party-platform-presences /openapi.json get /v1/podcasts/{id}/external-links Returns every third-party platform on which the podcast has a known presence — podcast directories (Apple Podcasts, Spotify, Castbox, …), social profiles (X, Instagram, TikTok, …), video channels (YouTube), and the publisher's own website. Each entry includes the platform-native identifier, a resolved web URL, and a list of optional attributes (audience size, profile metadata, account status). The shape is uniform across platforms; new attributes may be added over time without breaking existing clients. # List and search podcasts Source: https://docs.particle.pro/api-reference/podcasts/list-and-search-podcasts /openapi.json get /v1/podcasts Returns a paginated list of podcasts. Pass `q` to search by name (typos and extra words are fine; description-only matches rank last and are labeled by match_quality) and filter by topic, language, or suitability tier. # List entity mentions in a podcast Source: https://docs.particle.pro/api-reference/podcasts/list-entity-mentions-in-a-podcast /openapi.json get /v1/podcasts/{id}/mentions Returns episodes where a specific entity appears within a podcast, ordered by most recent. Each result includes the episode, salience score, occurrence count, and speaker roles. Requires entity_id or company_id. # List episodes for a podcast Source: https://docs.particle.pro/api-reference/podcasts/list-episodes-for-a-podcast /openapi.json get /v1/podcasts/{id}/episodes Returns a paginated list of episodes for a specific podcast, identified by slug (e.g., 'all-in') or ID. # List podcast topics Source: https://docs.particle.pro/api-reference/podcasts/list-podcast-topics /openapi.json get /v1/podcasts/topics Returns the top-level topics covered across all podcasts, sorted by the number of podcasts covering each topic. # Look up podcasts by external platform identifier Source: https://docs.particle.pro/api-reference/podcasts/look-up-podcasts-by-external-platform-identifier /openapi.json get /v1/podcasts/lookup Resolves one or more external platform identifiers (Apple Podcasts collection IDs, Spotify show IDs, YouTube channel IDs, …) — or RSS feed URLs via `platform=rss` — to Particle podcasts. Pass `platform` once and a comma-separated list of identifiers (up to 100 per call). Each result echoes the input identifier alongside the matched podcast; unresolved identifiers omit the `podcast` field entirely so bulk callers can correlate inputs and outputs by key presence without doing a separate join. Use this when you already hold platform-native IDs and want a deterministic mapping; for fuzzy name search use GET /v1/podcasts?q=. # Get a topic Source: https://docs.particle.pro/api-reference/topics/get-a-topic /openapi.json get /v1/topics/{id} Returns a single topic with breadcrumb ancestors and the top direct children by prominence. Children are capped — use /v1/topics?parent_id={id} to paginate beyond the cap. # List topics Source: https://docs.particle.pro/api-reference/topics/list-topics /openapi.json get /v1/topics Returns the hierarchical topic taxonomy. Use parent_id to navigate the tree. # Agent authentication Source: https://docs.particle.pro/auth The auth.md agent-onboarding recipe — discovery, the credential decision tree, registration, usage, errors, and revocation — for agents connecting to Particle Pro. This page mirrors the machine-readable onboarding document served at [`https://api.particle.pro/auth.md`](https://api.particle.pro/auth.md) and [`https://mcp.particle.pro/auth.md`](https://mcp.particle.pro/auth.md), following the [auth.md protocol](https://github.com/workos/auth.md). If you are an agent reading this: the steps below are authoritative and in order. Particle Pro has two protected surfaces: * **MCP** at `https://mcp.particle.pro` — the Model Context Protocol server (podcast intelligence, companies, people, topics). * **REST API** at `https://api.particle.pro` — the same data over plain HTTP under `/v1/`. And two credential types, matched to two kinds of callers: * **OAuth 2.1 access tokens** — for interactive MCP clients (Claude Code, Cursor, ChatGPT, Claude Desktop). Short-lived JWTs, audience-bound to `https://mcp.particle.pro`. A human approves the connection once in a browser. * **`pp_*` platform API keys** — for headless agents and all REST API calls. Minted by a human in the [platform dashboard](https://platform.particle.pro/tokens). There is **no anonymous agent self-registration**: every credential is tied to a human-owned platform account. The fastest path for a fully headless agent is to ask your operator for an API key. ## Step 1 — Discover Discovery is two hops. Fetch the RFC 9728 protected-resource metadata at the resource you want to call: ```http theme={"dark"} GET https://mcp.particle.pro/.well-known/oauth-protected-resource GET https://api.particle.pro/.well-known/oauth-protected-resource ``` A 401 from the MCP server also carries a `WWW-Authenticate: Bearer resource_metadata="…"` header pointing at the first document. Then follow `authorization_servers` to the RFC 8414 Authorization Server metadata: ```http theme={"dark"} GET https://api.particle.pro/.well-known/oauth-authorization-server ``` The `agent_auth` block in that document is the machine-readable summary of this page — see [Authentication → Agent discovery](/mcp/authentication#agent-discovery) for the full block. ## Step 2 — Pick a method 1. **A human is present to approve the connection, and you speak MCP** → OAuth 2.1 (Step 3). 2. **You are headless, or you call the REST API** → `pp_*` API key (Step 4). OAuth access tokens are **not** accepted by the REST API — they are audience-bound to the MCP resource. ## Step 3 — OAuth 2.1 (interactive MCP clients) 1. **Register a client**: `POST https://api.particle.pro/oauth/register` (RFC 7591 dynamic client registration), or bring a client-id metadata document URL (CIMD) as your `client_id`. Registering a client does **not** issue a credential — a human must still approve. 2. **Authorize**: `GET https://api.particle.pro/oauth/authorize` with `response_type=code`, PKCE `code_challenge_method=S256` (mandatory), and `resource=https://mcp.particle.pro` (mandatory). The user approves and selects a project at platform.particle.pro. 3. **Exchange**: `POST https://api.particle.pro/oauth/token` → access token (15-minute JWT) plus a refresh token (30 days, rotated on every use). Stock MCP clients run this flow automatically — see the [Quickstart](/mcp/quickstart). Full protocol detail: [Authentication](/mcp/authentication). ## Step 4 — API key (headless agents and the REST API) A human operator: 1. Creates an account at [platform.particle.pro](https://platform.particle.pro) (email verification required). 2. Creates or joins an organization and project. 3. Mints a key at [platform.particle.pro/tokens](https://platform.particle.pro/tokens). The full `pp_…` key is shown exactly once. Key management is a signed-in human action in the dashboard — a `pp_*` key cannot mint, roll, or revoke credentials, including itself. ## Step 5 — Use the credential * **MCP**: `Authorization: Bearer ` or `X-API-Key: pp_…` on every request to `https://mcp.particle.pro`. * **REST**: `X-API-Key: pp_…` or `Authorization: Bearer pp_…` (or an `api-key` query parameter) on the `https://api.particle.pro/v1/…` data endpoints. ## What is NOT supported Declared here so you do not waste requests probing: * **Anonymous registration** — no credential is issued without a human-owned account. * **Identity assertion / ID-JAG** — no RFC 7523 assertion exchange. * **Device authorization grant** — no RFC 8628 device flow; headless means API key. * **OTP claim ceremony** — the consent screen in Step 3 is the approval step. * **OAuth tokens on the REST API** — access tokens are MCP-audience only. ## Errors * MCP 401s carry `WWW-Authenticate: Bearer realm="mcp", error="invalid_token", resource_metadata="…"` with a human-readable reason in the body — see [MCP errors](/mcp/errors). * REST errors are RFC 9457 `application/problem+json` with a stable `error_code`. When self-service resolution is available, a `resolve` object is included telling you (or your operator) how to fix the problem — see the [error catalog](/errors/overview). ## Revocation * Refresh token (revokes its whole grant chain): `POST https://api.particle.pro/oauth/revoke` (RFC 7009). * An OAuth connection (grant): a signed-in human revokes it under [Connected Applications](https://platform.particle.pro) — see [Manage connections](/mcp/authentication#manage-connections). * An API key: a signed-in human revokes it in the [dashboard](https://platform.particle.pro/tokens). * Access tokens are stateless 15-minute JWTs: revoking the grant stops refresh, and outstanding tokens expire on their own. # Companies Source: https://docs.particle.pro/companies/overview Cross-referenced company profiles with SEC, Wikidata, ticker, domain, and knowledge-graph identifiers. Companies in Particle API map across every identifier system you might already have. Look up Nvidia by ticker, by domain, by SEC CIK, or by knowledge-graph slug — you'll always land on the same record. From there you can pull a structured product hierarchy, follow the company's appearances across podcasts, fetch its competitors, or pull sponsor analytics for its ad presence. Available to MCP agents as [`particle_company_resolve`](/mcp/tools/companies/company-resolve) and [`particle_company_get`](/mcp/tools/companies/company-get). ## Identifiers Every company carries a single `identifiers` block: | Identifier | Source | Example | | --------------------------- | ---------------------- | ------------ | | `ticker` | Primary stock ticker | `NVDA` | | `cik` | SEC Central Index Key | `0001045810` | | `qid` | Wikidata QID | `Q182477` | | `domain` | Company domain | `nvidia.com` | | `entity_id` / `entity_slug` | Knowledge-graph entity | `nvidia` | Use any of them as a query filter on `list-companies`. The slug, domain, and canonical ID resolve directly in the `{id}` slot of singular endpoints; for ticker, CIK, or QID, query `GET /v1/companies?ticker=…` (or `?cik=…` / `?qid=…`) first and pass the returned slug, domain, or ID through the singular endpoint. ## List companies ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/companies?ticker=NVDA&limit=1" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/companies?ticker=NVDA&limit=1", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/companies", params={"ticker": "NVDA", "limit": 1}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```json Response theme={"dark"} { "data": [ { "id": "3CensCwu5G2oKCFgPrNf89", "name": "Nvidia", "description": "Nvidia is a leading developer of graphics processing units…", "updated_at": "2026-02-15T08:41:32Z", "identifiers": { "cik": "0001045810", "qid": "Q182477", "entity_id": "nvidia", "entity_slug": "nvidia", "ticker": "NVDA", "domain": "nvidia.com" } } ], "has_more": false } ``` ### Filter parameters | Parameter | Description | Example | | --------------- | ------------------------------------------------------------------------------------- | ------------------------------------- | | `q` | Case-insensitive name search | `?q=Apple` | | `ticker` | Stock ticker(s), comma-separated, up to 100 | `?ticker=AAPL,GOOG` | | `domain` | Domain(s), comma-separated, up to 100 | `?domain=apple.com,nvidia.com` | | `cik` | SEC CIK(s), comma-separated, up to 100 | `?cik=0000320193,0001045810` | | `qid` | Wikidata QID(s), comma-separated, up to 100 | `?qid=Q312,Q182477` | | `entity_id` | Knowledge-graph slug(s) or ID(s), comma-separated, up to 100 | `?entity_id=apple,nvidia` | | `ids` | Bulk multi-get by company slug, domain, or ID, comma-separated, up to 100 (see below) | `?ids=apple,tesla.com,nvidia` | | `updated_after` | Incremental sync filter | `?updated_after=2026-04-01T00:00:00Z` | ```bash theme={"dark"} # Bulk lookup by tickers curl "https://api.particle.pro/v1/companies?ticker=AAPL,NVDA,MSFT" \ -H "X-API-Key: $PARTICLE_API_KEY" # Incremental sync curl "https://api.particle.pro/v1/companies?updated_after=2026-04-01T00:00:00Z" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ### Fetch many at once When you already hold a set of company slugs, domains, or IDs — the same identifiers `/v1/companies/{id}` accepts — pass them as a comma-separated `ids` list to fetch them all in a single request instead of one call per company, up to 100 per call. Companies come back in the same shape and in the order you asked for them. A ref that doesn't resolve is simply left out (no error, no placeholder), so compare the returned identifiers against what you sent to find any that are missing. ```bash theme={"dark"} curl "https://api.particle.pro/v1/companies?ids=apple,tesla.com,3CensCwu5G2oKCFgPrNf89" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Unlike `entity_id` (which filters companies by their linked knowledge-graph entity), `ids` is a direct multi-get of companies by their own identifier. When `ids` is present the other filters and pagination are ignored. ## Get a single company ```bash theme={"dark"} curl "https://api.particle.pro/v1/companies/nvidia" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` `nvidia` (slug), `nvidia.com` (domain), and `3CensCwu5G2oKCFgPrNf89` (canonical ID) all resolve directly via `/v1/companies/{id}` to the same response. To look up a company by `NVDA` (ticker), `0001045810` (CIK), or `Q182477` (QID), call `GET /v1/companies` with the matching query filter (`?ticker=NVDA`, `?cik=…`, `?qid=…`) and use the returned slug, domain, or canonical ID with the singular endpoint. ## Sub-resources | Endpoint | Returns | | -------------------------------------------- | --------------------------------------------------- | | `GET /v1/companies/{id}/products` | Three-level product hierarchy with lifecycle status | | `GET /v1/companies/{id}/competitors` | Competitor list | | `GET /v1/companies/{id}/podcast/advertising` | Sponsor analytics for the company's ad presence | See [Products](/companies/products) for the product hierarchy and [Advertising](/podcasts/advertising#per-company-advertising) for the sponsor analytics shape. ## Cross-referencing with the knowledge graph The `entity_slug` field on every company doubles as a knowledge-graph handle. Use it to find every podcast appearance, mention, and clip where the company is discussed: ```bash theme={"dark"} # Episodes featuring or mentioning Nvidia curl "https://api.particle.pro/v1/podcasts/episodes?company_id=nvidia&limit=5" \ -H "X-API-Key: $PARTICLE_API_KEY" # Every line of dialogue mentioning Nvidia, grouped by episode curl "https://api.particle.pro/v1/podcasts/mentions?company_id=nvidia&limit=5" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` The `company_id` filter on the episodes and mentions endpoints accepts a slug, domain, or canonical ID — the same handles that resolve directly via `/v1/companies/{id}`. ## Related * [Products](/companies/products) — recursive product tree with lifecycle status * [Podcast advertising](/podcasts/advertising) — `/companies/{id}/podcast/advertising` and the leaderboard * [Knowledge graph → entities](/knowledge-graph/entities) — follow a company across audio content # Products Source: https://docs.particle.pro/companies/products Three-level product hierarchies with lifecycle status. Every company has a structured product tree: **segments** contain **product lines**, which contain individual **products**. Each node carries a lifecycle `status` (`active`, `announced`, `discontinued`, `rumored`) and, where available, a canonical `product_url`. ## Hierarchy | Level | What it is | Example (Nvidia) | | -------------- | ----------------------------- | -------------------------- | | `segment` | Top-level business segment | `Compute & Networking` | | `product_line` | Product line within a segment | `Data Center Accelerators` | | `product` | Individual product or service | `Blackwell GPUs` | ## Lifecycle statuses | Status | Meaning | | -------------- | ------------------------------ | | `active` | Currently available | | `announced` | Announced but not yet released | | `discontinued` | No longer available | | `rumored` | Reported but not confirmed | ## Get a company's products ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/companies/nvidia/products" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/companies/nvidia/products", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const segments = await res.json(); ``` The response uses the standard list envelope. `data` is the array of root-level `segment` nodes; each node may contain `children` of the next level down, recursively. ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "6BsVni5re7N3JP5H5BzVjD", "name": "Automotive", "level": "segment", "status": "active", "children": [ { "name": "NVIDIA DRIVE", "level": "product_line", "status": "active", "children": [ { "name": "DRIVE AGX Hardware & OS", "level": "product", "status": "active" }, { "name": "DRIVE Hyperion Platform", "level": "product", "status": "active" }, { "name": "DRIVE Software Suite", "level": "product", "status": "active" } // … ] } // … ] }, { "id": "4P3oLUiJTgz5dcjdwy9iLb", "name": "Compute & Networking", "level": "segment", "status": "active", "children": [ { "name": "Data Center Accelerators", "level": "product_line", "status": "active", "children": [ { "name": "Blackwell GPUs", "level": "product", "status": "active" }, { "name": "Hopper GPUs", "level": "product", "status": "active" }, { "name": "Grace and Vera CPUs", "level": "product", "status": "active" } // … ] } // … ] } // … ], "has_more": false, "cursor": null } ``` ## Filtering by status By default only `active` products are returned. Pass `status` as a comma-separated list to include other lifecycle stages: ```bash theme={"dark"} # Active and announced curl "https://api.particle.pro/v1/companies/nvidia/products?status=active,announced" \ -H "X-API-Key: $PARTICLE_API_KEY" # Only discontinued curl "https://api.particle.pro/v1/companies/nvidia/products?status=discontinued" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Tree integrity is preserved when filtering: if a parent doesn't match the status filter it's removed along with its children, even if some children individually match. To traverse without parent constraints, request all statuses and filter on your side. ## Common patterns ### Flatten to a list of products ```js theme={"dark"} function flattenProducts(nodes, out = []) { for (const node of nodes) { if (node.level === "product") out.push(node); if (node.children) flattenProducts(node.children, out); } return out; } const { data: segments } = await fetch(".../v1/companies/nvidia/products", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }).then((r) => r.json()); const products = flattenProducts(segments); ``` ### Track product launches Combine `?status=announced` with periodic polling to see when companies announce new products. Combine with `?status=discontinued` to detect end-of-life. ## Related * [Companies overview](/companies/overview) — resolve any company before fetching products * [Knowledge graph → entities](/knowledge-graph/entities) — products are linked to entities for cross-content discovery # Concepts Source: https://docs.particle.pro/concepts Cross-cutting conventions: IDs, pagination, errors, pricing weight, and choosing the right endpoint. This page is the cheat sheet every other guide refers back to. Skim it once, then look up specifics as needed. ## IDs and slugs Most endpoints with an `{id}` path parameter accept either: * The canonical opaque ID (e.g. `17PzxG1t12xzno` for Sam Altman, `3CensCwu5G2oKCFgPrNf89` for Nvidia), or * A human-readable slug (e.g. `sam-altman`, `nvidia`, `pivot`, `the-joe-rogan-experience`). Use whichever you have. If you receive an entity slug from one response (`entity_slug: "nvidia"`), you can pass it directly to any other endpoint without first looking up the canonical ID. ```bash theme={"dark"} # Both of these resolve to the same entity: curl ".../v1/entities/sam-altman" -H "X-API-Key: $PARTICLE_API_KEY" curl ".../v1/entities/17PzxG1t12xzno" -H "X-API-Key: $PARTICLE_API_KEY" ``` Companies are richer: `/v1/companies/{id}` resolves slug, domain, *and* canonical ID directly. To look up by ticker, CIK, or QID, use the corresponding query filter on `GET /v1/companies` (`?ticker=…`, `?cik=…`, `?qid=…`) and pass the returned slug, domain, or ID through the singular endpoint. See [Companies → Identifiers](/companies/overview#identifiers). Podcasts accept one extra form: every podcast `{id}` slot (and the `podcast_id` filter on list endpoints) also resolves a numeric **Apple/iTunes collection ID** directly (e.g. `1535809341`) — so a client that already holds an iTunes ID can call any podcast endpoint without a prior lookup. To resolve a **Spotify show ID**, **YouTube channel ID**, **RSS feed URL**, or other platform identifier, use [`GET /v1/podcasts/lookup`](/podcasts/lookup). ## Cursor pagination List endpoints share a single response envelope: ```json theme={"dark"} { "data": [ /* … */ ], "has_more": true, "cursor": "r.4gfFC7" } ``` To fetch the next page, pass the `cursor` value back as a query parameter: ```bash theme={"dark"} curl ".../v1/podcasts?cursor=r.4gfFC7" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Cursors are opaque — treat them as strings, do not parse or construct them, and do not assume any ordering of values across versions. The default page size is endpoint-specific (often 25); use `limit` to override (typically up to 100). ## Authentication Two header forms are accepted; `X-API-Key` is recommended. ```bash theme={"dark"} # Recommended curl -H "X-API-Key: $PARTICLE_API_KEY" "https://api.particle.pro/v1/podcasts" # Equivalent curl -H "Authorization: Bearer $PARTICLE_API_KEY" "https://api.particle.pro/v1/podcasts" ``` Embed endpoints (`/v1/embed/*`) intentionally do not require authentication so the responses can be used as public iframe payloads. ## Pricing weight Every endpoint is available on every account — there is no tier lock. Endpoints are *priced* differently, though: heavier endpoints (full transcripts, transcript mentions, advertising analytics, cross-podcast clip search, competitor lookups) consume more credits per call than lighter ones (entity, topic, and podcast metadata; episode lookups and sub-resources; clip listings; embed). The OpenAPI tag on every operation declares the pricing class — `tier:standard` or `tier:premium`. Treat it as a hint about cost, not a hint about access. Your usage dashboard breaks down spend by endpoint so you can see where credits are going. ## Choosing the right endpoint The API exposes overlapping ways to find content. Pick by what you actually need. | You want… | Use | Notes | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | Dialogue matching the *meaning* of a query, paraphrase-tolerant | `GET /v1/podcasts/episodes/search?semantic_search=…` | Vector similarity. Express the topic the way you'd describe it; speakers may use different vocabulary. | | Dialogue containing exact tokens or phrases | `GET /v1/podcasts/episodes/search?keyword_search=…` | BM25. Use for proper nouns, tickers, drug names, product codes. | | Every dialogue line where a person or company is mentioned | `GET /v1/podcasts/mentions?entity_id=…` | Episode-grouped mention windows with `is_mention` flags. Use for the read-everything-about-X workflow. | | Combine ranked search with an entity scope | `GET /v1/podcasts/episodes/search?semantic_search=…&entity_id=…` | Ranked candidates filtered to episodes where the entity appears. | | Episode-level filtering by entity, topic, language, or date | `GET /v1/podcasts/episodes?entity_id=…` | Standard list, supports rich filters. Use when you don't need dialogue. | | All entities mentioned in one episode | `GET /v1/podcasts/episodes/{id}/transcript/mentions` | Per-episode rollup with surrounding-context windows. | | Salience rollup of an entity across one podcast's episodes | `GET /v1/podcasts/{id}/mentions` | Episode-level aggregate within one podcast. | | AI-segmented structural sections of an episode | `GET /v1/podcasts/episodes/{id}/segments` | Intros, ads, topic discussions, interviews. | | Browse highlight clips ranked by engagement | `GET /v1/podcasts/clips` | Curated highlights. Clips matching dialogue content come back inside `/v1/podcasts/episodes/search` results. | | Who advertises on which podcasts | `GET /v1/companies/{id}/podcast/advertising` and `/v1/podcasts/advertising/leaderboard` | Sponsor-side analytics. | [Quickstart](/quickstart) walks through several of these patterns end-to-end. ## Errors Errors follow [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) (Problem Details for HTTP APIs). Every error response is `application/problem+json` with at minimum `status`, `title`, and `detail`, plus a stable `error_code` for programmatic branching and an optional `resolve` object pointing at how to fix the issue. See [Errors → Overview](/errors/overview) for the full envelope, the catalog of error codes, and patterns for handling them in UI clients and agents. ## Rate limits Rate-limited responses return HTTP 429 with `error_code: "rate_limit_exceeded"`. Back off and retry — the response includes guidance in `detail`. See [`rate_limit_exceeded`](/errors/rate_limit_exceeded). # add_on_required Source: https://docs.particle.pro/errors/add_on_required Feature requires purchasing an add-on package | | | | ------------------ | ---------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `add_on_required` | | **Resolve action** | `purchase_add_on` | ## What happened The organization tried to use a feature beyond its plan's included allowance — for example, creating a monitor when all of the plan's included monitor slots are already in use. The plan's allowance can be extended by purchasing add-on packages: each Monitors Pack add-on increases the monitor allowance by five. ## How to fix Purchase the required add-on via the platform UI's billing page, or call the add-on endpoint directly: ``` POST /v1/organizations/{org_id}/billing/addons { "type": "monitor_pack", "quantity": 1 } ``` After the purchase succeeds, retry the original request. # api_key_required Source: https://docs.particle.pro/errors/api_key_required Valid API key required | | | | ------------------ | ------------------ | | **HTTP status** | `401 Unauthorized` | | **Error code** | `api_key_required` | | **Resolve action** | `create_api_key` | ## What happened The request did not include a valid API key. Content API endpoints (podcasts, entities, etc.) require an API key passed via `X-API-Key` HTTP header, the `Authorization: Bearer` HTTP header, or the `api-key` query parameter. ## How to fix Create a new API key from the API Keys page in the platform dashboard. Include your API key in your request using one of the [authentication mechanisms](/api-reference/introduction#authentication). ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/projects \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "My Project"}' ``` ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/projects/{projectId}/api-keys \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Production"}' ``` The full key is returned **only in this response** — store it securely. ```bash theme={"dark"} curl https://api.particle.pro/v1/podcasts \ -H "X-API-Key: YOUR_API_KEY" ``` # auth_required Source: https://docs.particle.pro/errors/auth_required Authentication required | | | | ------------------ | ------------------ | | **HTTP status** | `401 Unauthorized` | | **Error code** | `auth_required` | | **Resolve action** | `login` | ## What happened The request did not include valid user credentials. This error is returned by Particle Pro platform endpoints that require a logged-in user session (JWT). Content API endpoints use [`api_key_required`](/errors/api_key_required) instead. ## How to fix If you are a human user, log in to the platform to obtain a valid session. If you are building an integration, use an API key instead. Create one from the API Keys page and authenticate via the `X-API-Key` header or `Authorization: Bearer` header. # bad_request Source: https://docs.particle.pro/errors/bad_request Malformed request | | | | --------------- | ----------------- | | **HTTP status** | `400 Bad Request` | | **Error code** | `bad_request` | ## What happened The request was rejected before it reached the handler — typically malformed JSON, an unsupported content type, or a required query/path parameter that could not be parsed. ## How to fix Inspect `detail` (and `errors` when present) for the exact reason, correct the request, and retry. # billing_info_required Source: https://docs.particle.pro/errors/billing_info_required Billing information is required for paid plans | | | | ------------------ | ----------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `billing_info_required` | | **Resolve action** | `setup_billing` | ## What happened The organization is trying to select or switch to a paid plan, but no payment method is on file. Free plans do not require billing information. ## How to fix Visit your organization's billing page to add a payment method. Once billing info is on file, retry the plan selection or change request. # cancellation_effective Source: https://docs.particle.pro/errors/cancellation_effective Cancellation has already taken effect — the Orb subscription is gone, so resume is not possible | | | | ------------------ | ------------------------ | | **HTTP status** | `409 Conflict` | | **Error code** | `cancellation_effective` | | **Resolve action** | `select_plan` | ## What happened `POST /v1/organizations/{orgId}/billing/subscription/resume` was called on an organization whose cancellation has already taken effect — the Orb subscription has ended, so there is nothing left to unschedule. This happens for PAYG cancellations (Starter, which cancel immediately) and for paid cancellations once the grace period has elapsed. ## How to fix Sign up again with `POST /v1/organizations/{orgId}/billing/subscription`. A new Orb subscription is created on the same Orb customer using the plan ID supplied in the request body. The previous `one_time_credit_granted` flag is preserved, so the signup credit is not granted twice. # cancellation_revertable Source: https://docs.particle.pro/errors/cancellation_revertable Subscription is canceled but the cancellation has not yet taken effect — resume to undo it | | | | ------------------ | ------------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `cancellation_revertable` | | **Resolve action** | `resume_subscription` | ## What happened `POST /v1/organizations/{orgId}/billing/subscription` (select-plan) or `PUT /v1/organizations/{orgId}/billing/subscription` (change-plan) was called on an organization whose subscription is canceled but the cancellation has not yet taken effect — the underlying Orb subscription is still active with a scheduled end date in the future. This is the typical state for paid plans (e.g. Growth) canceled mid-billing-period. Selecting or changing a plan in this state would strand the still-active Orb subscription with a pending cancellation, so the API rejects the request and points you at the resume endpoint instead. ## How to fix Call `POST /v1/organizations/{orgId}/billing/subscription/resume` to unschedule the cancellation. This restores `subscription_status` to `ACTIVE` and clears `canceled_at` — no new credit is granted, and the previous plan is preserved. To switch plans after resuming, follow up with `PUT /v1/organizations/{orgId}/billing/subscription`. # conflict Source: https://docs.particle.pro/errors/conflict Request conflicts with current state | | | | --------------- | -------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `conflict` | ## What happened The request cannot be completed because it conflicts with the current state of the resource — for example, creating something with an identifier or name that already exists. ## How to fix Reconcile the intended state with the current state (fetch the resource, pick a non-conflicting value, or apply the change to the existing record) and retry. # credits_depleted Source: https://docs.particle.pro/errors/credits_depleted Credit allocation exhausted for this billing period | | | | ------------------ | ---------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `credits_depleted` | | **Resolve action** | `upgrade_plan` | ## What happened The organization's credit allocation for the current billing period has been fully consumed. API requests are blocked until you upgrade to a higher plan or credits replenish at the start of the next billing period. ## How to fix #### Option 1: Upgrade to a plan with more credits Visit your organization's billing settings to change to a plan with more credits. ```bash theme={"dark"} curl -X PUT https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "HIGHER_TIER_PLAN_ID"}' ``` #### Option 2: Wait for credit replenishment Credits are replenished at the start of each billing period. The block expires automatically when the new period begins. Visit your organization's billing settings to view your current credit balance and billing period dates. # email_exists Source: https://docs.particle.pro/errors/email_exists Account already exists with this email | | | | ------------------ | -------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `email_exists` | | **Resolve action** | `login` | ## What happened An account with this email address already exists. Each email can only be registered once. ## How to fix Log in with your existing account via this email. If you own this email address, reset your password if needed. # email_verification_required Source: https://docs.particle.pro/errors/email_verification_required Email verification is required to complete registration | | | | ------------------ | ----------------------------- | | **HTTP status** | `200 OK` | | **Error code** | `email_verification_required` | | **Resolve action** | `verify_email` | ## What happened An email address needs to be verified before it can be used. This error is returned in two contexts: 1. **Account signup** — a verification email has been sent to the registering address; the account is not created until the link is clicked. 2. **Monitor notifications** — a monitor was created or updated with a notification email that has not been verified for the owning organization. The monitor write is rejected so that no monitor silently fails to deliver. ## How to fix ### Account signup Check your inbox for an email with the subject "Verify your email address". Click the verification link in the email. The link expires in 24 hours. After verification, log in with your credentials. If you did not receive the email, you can request a new one: Try registering again with the same email and password. This will resend the verification email and invalidate any previous verification links. Call `POST /v1/auth/register` again with the same credentials. This will resend the verification email and invalidate any previous verification links. ### Monitor notifications Start verification for the email address by calling `POST /v1/organizations/{orgId}/notification-emails` with `{"email": "..."}`. A confirmation link is sent to that address. Click the link (or `POST /v1/notification-emails/verify` with the token from the link). The link expires in 24 hours. Retry the monitor `POST` or `PATCH` — the email is now in the organization's verified pool and the write succeeds. Monitor notification channels that match the authenticated user's own email address are auto-verified the first time they are used, so no extra step is needed for self-delivery. # endpoint_not_in_plan Source: https://docs.particle.pro/errors/endpoint_not_in_plan This endpoint or tool is not included in your current plan | | | | ------------------ | ---------------------- | | **HTTP status** | `403 Forbidden` | | **Error code** | `endpoint_not_in_plan` | | **Resolve action** | `upgrade_plan` | ## What happened Your organization has an active plan, but the endpoint or MCP tool you called is not included in it, so access is restricted. Because the request is billable (authenticated with an API key or OAuth token), it is rejected rather than served. This typically occurs when: * Your plan tier doesn't include the endpoint or tool you're calling * A newer capability was added that your current plan doesn't include ## How to fix Upgrade to a plan that includes this endpoint from your organization's billing settings in the platform UI. ```bash theme={"dark"} curl https://api.particle.pro/v1/billing/plans \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```bash theme={"dark"} curl -X PUT https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID_FROM_STEP_1"}' ``` Requires the **OWNER** role. # feature_check_failed Source: https://docs.particle.pro/errors/feature_check_failed Feature availability check failed | | | | --------------- | --------------------------- | | **HTTP status** | `500 Internal Server Error` | | **Error code** | `feature_check_failed` | ## What happened An internal error occurred while verifying feature access. This is a transient server-side issue. ## How to fix Retry the request after a short delay. If the error persists, contact support at or visit the support page for direct support. # forbidden Source: https://docs.particle.pro/errors/forbidden Access denied | | | | --------------- | --------------- | | **HTTP status** | `403 Forbidden` | | **Error code** | `forbidden` | ## What happened The caller is authenticated but not permitted to perform this action — for example, an API key whose project does not own the requested resource, or a user without the required organization role. ## How to fix Verify that the authenticated user or API key has access to the target resource. For role-based errors, an owner of the organization can grant the necessary role. # internal_error Source: https://docs.particle.pro/errors/internal_error Unexpected server error | | | | --------------- | --------------------------- | | **HTTP status** | `500 Internal Server Error` | | **Error code** | `internal_error` | ## What happened An unexpected server-side error occurred. The request may or may not have been partially processed. ## How to fix Retry the request after a short delay. If the error persists, check for any ongoing incidents, then contact support at and include the `X-Trace-ID` response header so we can locate the failure. # invite_expired Source: https://docs.particle.pro/errors/invite_expired Organization invitation has expired | | | | ------------------ | ---------------- | | **HTTP status** | `410 Gone` | | **Error code** | `invite_expired` | | **Resolve action** | `request_invite` | ## What happened The organization invitation has expired. Invitations are valid for 7 days after being sent. ## How to fix Ask the organization admin or owner to send a new invitation. They can do this from your organization's members page. An admin can resend the invite via the API. Use the `inviteId` from `GET /v1/organizations/{orgId}/invites`: ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/invites/{inviteId}/resend \ -H "Authorization: Bearer ADMIN_JWT_TOKEN" ``` Or send a new invitation: ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/invites \ -H "Authorization: Bearer ADMIN_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "invitee@example.com", "role": "MEMBER"}' ``` Only organization owners and admins can send invitations. # no_active_plan Source: https://docs.particle.pro/errors/no_active_plan No active billing plan | | | | ------------------ | ---------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `no_active_plan` | | **Resolve action** | `select_plan` | ## What happened The organization does not have an active billing plan. All API requests that require a subscription are blocked until a plan is selected. This typically occurs when: * A new organization hasn't selected a plan yet * The free plan subscription failed during signup and needs to be retried ## How to fix Visit your organization's billing settings in the platform UI. ```bash theme={"dark"} curl https://api.particle.pro/v1/billing/plans \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID_FROM_STEP_1"}' ``` Requires the **OWNER** role. # no_active_subscription Source: https://docs.particle.pro/errors/no_active_subscription No active subscription for spend limit configuration | | | | ------------------ | ------------------------ | | **HTTP status** | `400 Bad Request` | | **Error code** | `no_active_subscription` | | **Resolve action** | `select_plan` | ## What happened Spend limits can only be configured on organizations with an active subscription. The organization doesn't have one yet. ## How to fix Ensure your organization has an active plan. Ensure your organization has billing information. ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID"}' ``` ```bash theme={"dark"} curl -X PUT https://api.particle.pro/v1/organizations/{orgId}/billing/spend-limits \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"monthly_budget_cents": 10000}' ``` # not_a_member Source: https://docs.particle.pro/errors/not_a_member Not a member of the organization | | | | ------------------ | ---------------- | | **HTTP status** | `403 Forbidden` | | **Error code** | `not_a_member` | | **Resolve action** | `request_invite` | ## What happened The authenticated user is not a member of the organization they are trying to access. Organization resources (projects, billing, members, etc.) are only accessible to members. ## How to fix Ask an organization admin or owner to send you an invitation: 1. The admin visits your organization's members page. 2. They send an invite to your email. 3. You accept the invite from your email and log in successfully. 1. The admin sends an invite: ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/invites \ -H "Authorization: Bearer ADMIN_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "invitee@example.com", "role": "MEMBER"}' ``` 2. The invitee accepts via the token from the invite email: ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/invites/{token}/accept \ -H "Authorization: Bearer INVITEE_JWT_TOKEN" ``` Invitations expire after 7 days. If your invite has expired, ask the admin to send a new one. # not_found Source: https://docs.particle.pro/errors/not_found Resource not found | | | | --------------- | --------------- | | **HTTP status** | `404 Not Found` | | **Error code** | `not_found` | ## What happened The requested resource does not exist or is not accessible to the caller. Identifier typos, deleted resources, and cross-tenant access all surface as `not_found`. ## How to fix Check that the identifier in the URL is correct and that the authenticated caller has access to it. # notification_email_in_use Source: https://docs.particle.pro/errors/notification_email_in_use The notification email is still referenced by one or more monitors | | | | ------------------ | ---------------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `notification_email_in_use` | | **Resolve action** | `edit_monitor_notifications` | ## What happened You tried to delete a notification email that is still listed as a delivery target on one or more monitors in the organization. The system refuses to remove it so monitors don't silently stop delivering. ## How to fix Monitors are project-scoped, not org-scoped, so locating every monitor that references the email is a two-step traversal: list the org's projects, then list monitors per project. List the projects in the organization: `GET /v1/organizations/{orgId}/projects`. For each project, list its monitors: `GET /v1/projects/{projectId}/monitors`. Inspect each monitor's `notifications` array for the email you're deleting. For each monitor that references the email, `PATCH /v1/monitors/{id}` with a new `notifications` payload that omits it. Retry `DELETE /v1/notification-emails/{id}`. Or open the organization's monitor list in the platform UI at `{baseURL}/organizations/{orgId}/monitors` and remove the email from each affected monitor there. # Error reference Source: https://docs.particle.pro/errors/overview Structured error responses and how to handle them The Particle API returns structured error responses following [RFC 9457 (Problem Details for HTTP APIs)](https://datatracker.ietf.org/doc/html/rfc9457). Every error includes machine-readable fields for programmatic handling and human-readable fields for display. ## Error response format ```json theme={"dark"} { "type": "https://docs.particle.pro/errors/no_active_plan", "title": "Payment Required", "status": 402, "detail": "No active billing plan.", "error_code": "no_active_plan", "resolve": { "message": "Select a billing plan to continue.", "url": "https://platform.particle.pro/organizations/{orgId}/billing", "action": "select_plan", "method": "POST", "endpoint": "/v1/organizations/{orgId}/billing/subscription" } } ``` ### Standard fields (RFC 9457) | Field | Type | Description | | -------- | ------- | ------------------------------------------------------------------------------------- | | `type` | string | URI linking to this error's documentation page. | | `title` | string | Short, static summary (e.g. "Payment Required"). Does not change between occurrences. | | `status` | integer | HTTP status code. | | `detail` | string | Human-readable explanation of what went wrong. | ### Extension fields | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------- | | `error_code` | string | Stable, machine-readable identifier. Use this for programmatic branching. | | `resolve` | object | Actionable resolution guidance. Omitted when no self-service fix exists. | ### Resolve object | Field | Type | Description | | ---------- | ------ | ---------------------------------------------------------------------------------------------- | | `message` | string | Human-readable call-to-action (e.g. "Select a billing plan to continue."). | | `url` | string | Deep link into the platform UI where a human can fix the issue. | | `action` | string | Machine-readable action type for agents (e.g. `select_plan`). | | `method` | string | HTTP method for the resolution endpoint. | | `endpoint` | string | API path an agent can call to fix the issue. | ## Handling errors ### For UI clients 1. Switch on `error_code` to render the appropriate component or message. 2. Use `resolve.url` to link the user to the relevant platform page. 3. Fall back to displaying `detail` if the `error_code` is unrecognized. ### For agents and SDKs 1. Switch on `error_code` to decide the remediation path. 2. Use `resolve.action` as a semantic intent (e.g. `select_plan`, `contact_support`). 3. Call `resolve.method` + `resolve.endpoint` to fix the issue programmatically. 4. Dereference `type` for documentation on the specific error. ## Error codes by category ### Billing & subscription No billing plan selected. Payment method needed for paid plans. A plan is required before creating projects. Payment is overdue. Subscription suspended for non-payment. Subscription is not active. Billing subscription missing. No subscription exists for spend limit configuration. A plan must be selected before performing this action. Current plan does not allow usage beyond included limits. ### Usage limits Monthly budget cap reached. Credit balance exhausted. Too many requests — rate limit exceeded. ### Authentication & authorization No valid authentication provided. No valid API key provided. Pro subscription needed. Not a member of the organization. ### Account Account already exists with this email. Reset token is invalid. Reset token has expired. Account uses social login. Organization invite has expired. Email verification required to complete registration. ### Generic These codes are returned when a more specific code does not apply — typically for request validation, missing resources, and unexpected failures. Request was malformed or unparseable. Request body or parameters failed schema validation. Requested resource does not exist or is not accessible. Request conflicts with the current state of the resource. Authenticated caller is not permitted to perform this action. Unexpected server error — retry, and contact support if it persists. ### Internal Feature availability check failed. # payment_past_due Source: https://docs.particle.pro/errors/payment_past_due Payment is past due | | | | ------------------ | ----------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `payment_past_due` | | **Resolve action** | `update_payment_method` | ## What happened The organization's subscription payment has failed. API access is blocked until the payment issue is resolved. ## How to fix Visit your organization's billing page to update the payment method on file. The organization owner can update payment details from there. Once a successful payment is processed, API access is restored automatically. # pending_plan_change_exists Source: https://docs.particle.pro/errors/pending_plan_change_exists Operation blocked because a deferred plan change is scheduled to take effect at end-of-term | | | | ------------------ | ---------------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `pending_plan_change_exists` | | **Resolve action** | `cancel_pending_plan_change` | ## What happened The organization has a deferred plan change scheduled for the end of the current billing period (typically a paid → free downgrade), and the requested operation can't run safely while that schedule exists. Today the only operation that returns this error is `POST /v1/organizations/{org_id}/billing/addons`. The underlying constraint: at the scheduled change date, Orb resets each price interval's `fixed_price_quantity` to the *target* plan's defaults. Any add-on quantity bumped between now and the change date would be silently overwritten — the customer would pay for capacity they never effectively used. We reject up front rather than let that happen quietly. ## How to fix Cancel the pending plan change first, then make the add-on adjustment: ``` POST /v1/organizations/{org_id}/billing/pending-plan-change/cancel ``` The response carries the refreshed `Organization` with `pending_plan` and `pending_plan_effective_at` cleared. After this, `POST /billing/addons` is unblocked. You can also visit your organization's billing settings to cancel the pending change from the UI, or simply wait for the change date — once Orb applies the deferred transition, the schedule is gone and add-on adjustments are fine again. If you only wanted to change plans (not adjust add-ons), `PUT /v1/organizations/{org_id}/billing/subscription` with a new target overwrites any existing pending change in place — no cancel-then-PUT round-trip needed. # plan_change_blocked Source: https://docs.particle.pro/errors/plan_change_blocked Plan change cannot be applied because current usage exceeds the target plan's allowance | | | | ------------------ | --------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `plan_change_blocked` | | **Resolve action** | `check_feasibility` | ## What happened The requested plan change would leave the organization over the target plan's allowance even after applying any add-on adjustments in your request and auto-fill. For example, downgrading from Growth to Starter while seven monitors are active with explicit `monitor_pack: 1` only buys 6 monitors of capacity on Starter, leaving the org one monitor over. The plan change is rejected so the caller can resolve the overage explicitly — either by raising the add-on quantity, by reducing usage, or by re-attempting without explicit addons so auto-fill can size the pack correctly. ## How to fix In the platform UI, visit your organization's billing settings to preview the plan change — the UI surfaces the same blockers, end-state preview, and suggested resolutions and lets you adjust add-ons inline. To resolve programmatically, POST to the feasibility endpoint to discover what blocks the change and preview a fix: ``` POST /v1/organizations/{org_id}/billing/plan-change/feasibility { "target_plan": "particle_pro_pay_as_you_go", "addons": [ { "type": "monitor_pack", "quantity": 2 } ] } ``` The `addons[]` array is optional. Omit it to preview the auto-fill defaults; include it to test a specific configuration. The response includes: * `feasible` — `true` once the previewed end state covers current usage. * `end_state` — full preview of the post-change subscription: tier, allowances, add-on quantities, monthly cost, pricing breakdown. * `delta` — signed differences vs. current state (monthly cost, monitor allowance, per-add-on quantity deltas). * `blockers` — hard problems that prevent the plan change. Two types today: `monitor_overage` (carries `current` vs. end-state-`allowed` counts) and `already_on_plan` (the request's `target_plan` matches the org's current plan; route through `POST /billing/addons` if the goal was to adjust add-on quantities rather than change the plan). * `consequences` — non-blocking side-effects to acknowledge (e.g. `radar_lost`). * `suggested_resolutions` — per-blocker action lists. Largely redundant once you adjust the request `addons[]` and re-check. Once the response reports `feasible: true`, retry the plan change with the same `addons[]` body via `PUT /billing/subscription`. # plan_does_not_support_monitors Source: https://docs.particle.pro/errors/plan_does_not_support_monitors The organization's plan does not include the monitors feature | | | | ------------------ | -------------------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `plan_does_not_support_monitors` | | **Resolve action** | `upgrade_plan` | ## What happened The organization tried to create a monitor on a plan that does not include the monitors feature (**Starter** or **Growth**). Monitors are available only on the current plans — Team, Business, and Enterprise. Organizations on one of these plans keep any monitors they already have; only the creation of *new* monitors is blocked until the organization moves to a plan that includes monitors. ## How to fix Upgrade to a current plan via the platform UI's billing page, or change the plan directly: ``` PUT /v1/organizations/{org_id}/billing/subscription { "plan_id": "team" } ``` After the upgrade takes effect, retry the create-monitor request. # plan_does_not_support_overage_usage Source: https://docs.particle.pro/errors/plan_does_not_support_overage_usage The current plan does not support overage usage configuration | | | | ------------------ | ------------------------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `plan_does_not_support_overage_usage` | | **Resolve action** | `upgrade_plan` | ## What happened Your current plan does not support overage usage configuration. ## How to fix Overage usage is only supported on the Growth plan. Other plans cap usage at the plan limit and do not support additional billing beyond that. If you are on the Growth plan: Navigate to your organization's billing page. Enable the "Extra Usage" toggle to allow additional usage. Optionally, set an overage limit to stop overage spending at some fixed amount. Switch to a plan that supports overage configuration, then enable overage: ```bash theme={"dark"} curl -X PUT https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID"}' ``` ```bash theme={"dark"} curl -X PATCH https://api.particle.pro/v1/organizations/{orgId} \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"overage_enabled": true}' ``` # plan_not_available Source: https://docs.particle.pro/errors/plan_not_available The requested plan is deprecated and not available for new subscriptions | | | | ------------------ | -------------------------------------------------------------------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `plan_not_available` | | **Resolve action** | `select_plan` (initial selection, `POST`) or `upgrade_plan` (plan change, `PUT`) | ## What happened The organization tried to **select or change to a plan that has been deprecated**. A deprecated plan accepts no new subscriptions — it is kept only for the organizations already on it, which continue on it unchanged. This typically happens when a plan has been retired in favor of newer plans. ## How to fix Choose a currently available plan via the platform UI's billing page, or select/change to an available plan directly. Use `POST` for an initial selection and `PUT` for a plan change — the error's `resolve.method` tells you which applies to your case: ``` # initial selection (no active plan yet) POST /v1/organizations/{org_id}/billing/subscription { "plan_id": "team" } # changing from an existing plan PUT /v1/organizations/{org_id}/billing/subscription { "plan_id": "team" } ``` Organizations already subscribed to the deprecated plan are unaffected and do not need to take any action. # plan_not_selected Source: https://docs.particle.pro/errors/plan_not_selected Organization has no active plan selected | | | | ------------------ | ------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `plan_not_selected` | | **Resolve action** | `select_plan` | ## What happened A plan change was requested but the organization does not have an active plan yet. You must select an initial plan before you can switch to a different one. ## How to fix Navigate to your organization's billing page and verify that your organization has an active plan. Select an initial plan first: ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID"}' ``` Then use `PUT /v1/organizations/{orgId}/billing/subscription` to change plans. # plan_required Source: https://docs.particle.pro/errors/plan_required A billing plan is required to create projects | | | | ------------------ | ---------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `plan_required` | | **Resolve action** | `select_plan` | ## What happened Project creation requires an active billing plan. Since all organizations are auto-subscribed to the free plan on signup, this error only occurs if the billing subscription failed during registration. ## How to fix Navigate to your organization's billing page and verify that your organization has an active plan. Select a plan (including the free plan) to activate the organization: ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID"}' ``` Then retry project creation. # premium_required Source: https://docs.particle.pro/errors/premium_required Endpoint or tool requires a plan that includes premium endpoints | | | | ------------------ | ---------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `premium_required` | | **Resolve action** | `upgrade_plan` | ## What happened The organization called a **premium-tier** endpoint or MCP tool — for example Ad Intelligence, Publisher Landscape, or Brand Safety & Suitability — on a plan that does not include premium endpoints. Premium endpoints are available to every new organization while its one-time signup credit is funding usage. Once that credit is depleted and the subscription converts to its paid phase, premium access continues only on plans whose definition includes premium endpoints (for example Business). Plans without premium endpoints (for example Team) lose premium access at that point. ## How to fix Upgrade to a plan that includes premium endpoints via the platform UI's billing page, or change the plan directly: ``` PUT /v1/organizations/{org_id}/billing/subscription { "plan_id": "business" } ``` After the upgrade takes effect, retry the original request. # pro_required Source: https://docs.particle.pro/errors/pro_required Pro subscription required | | | | ------------------ | --------------- | | **HTTP status** | `403 Forbidden` | | **Error code** | `pro_required` | | **Resolve action** | `select_plan` | ## What happened The authenticated user does not have access to Particle API features. This endpoint requires an active Pro subscription. ## How to fix Navigate to your organization's billing page and verify that your organization has an active Pro subscription. ```bash theme={"dark"} curl https://api.particle.pro/v1/billing/plans \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```bash theme={"dark"} curl -X POST https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID"}' ``` Requires the **OWNER** role on the organization. # rate_limit_exceeded Source: https://docs.particle.pro/errors/rate_limit_exceeded API rate limit exceeded | | | | ------------------ | ------------------------------- | | **HTTP status** | `429 Too Many Requests` | | **Error code** | `rate_limit_exceeded` | | **Resolve action** | `upgrade_plan` (free tier only) | ## What happened The organization has exceeded the allowed number of API requests per minute. Rate limits are enforced per organization and vary by plan: | Plan | Limit | | ---- | ---------------------- | | Free | 1,000 requests/minute | | Paid | 10,000 requests/minute | ## Response headers Responses for API-key-authenticated requests include rate limit headers so clients can track their usage: | Header | Description | | ----------------------- | ------------------------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed per minute | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Reset` | Seconds until the rate limit window resets | | `Retry-After` | Seconds to wait before retrying (only on 429 responses) | ## How to fix Wait for the rate limit window to reset. The `Retry-After` response header indicates how many seconds to wait before retrying. Reduce your request rate or implement exponential backoff to avoid hitting the limit repeatedly. # seat_limit_exceeded Source: https://docs.particle.pro/errors/seat_limit_exceeded Organization has reached the seat cap for its plan | | | | ------------------ | ---------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `seat_limit_exceeded` | | **Resolve action** | `upgrade_plan` | ## What happened The organization has reached the seat cap defined by its current plan. Each plan caps the total of: * **active members** (users who have accepted an invite), plus * **pending, non-expired invites** (sent but not yet accepted). Pending invites count against the cap so concurrent invites can't bypass the limit. Cancelling a pending invite frees the seat immediately. Plan downgrades never remove existing members. If an organization exceeds its new cap after a downgrade, new seats stay blocked until the member count plus pending invites is below the cap again. ## How to fix Choose one: Upgrade the plan from your organization's billing page. Free a seat by removing a member or cancelling a pending invite. Upgrade the plan: ```bash theme={"dark"} curl -X PUT https://api.particle.pro/v1/organizations/{orgId}/billing/subscription \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan_id": "PLAN_ID"}' ``` Or free a seat by cancelling a pending invite: ```bash theme={"dark"} curl -X DELETE https://api.particle.pro/v1/organizations/{orgId}/invites/{inviteId} \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` Or removing a member: ```bash theme={"dark"} curl -X DELETE https://api.particle.pro/v1/organizations/{orgId}/members/{userId} \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` Requires the **OWNER** or **ADMIN** role. # social_login_only Source: https://docs.particle.pro/errors/social_login_only Account uses social login | | | | ------------------ | -------------------------- | | **HTTP status** | `422 Unprocessable Entity` | | **Error code** | `social_login_only` | | **Resolve action** | `login` | ## What happened The account was created using Google sign-in and does not have a password set. Password reset is not available for social login accounts. ## How to fix Visit the login page and click "Sign in with Google". # spend_limit_exceeded Source: https://docs.particle.pro/errors/spend_limit_exceeded Monthly spend limit exceeded | | | | ------------------ | ---------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `spend_limit_exceeded` | | **Resolve action** | `update_spend_limits` | ## What happened The organization's API usage has reached the configured monthly budget cap. All API requests are blocked until the spend limit is increased or the next billing period begins. ## How to fix Increase the spend limit: Navigate to your organization's billing page. Increase your organization's spend limit to a higher amount. Increase the limit: ```bash theme={"dark"} curl -X PUT https://api.particle.pro/v1/organizations/{orgId}/billing/spend-limits \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"monthly_budget_cents": 50000}' ``` Or remove the limit entirely: ```bash theme={"dark"} curl -X DELETE https://api.particle.pro/v1/organizations/{orgId}/billing/spend-limits \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` Both actions immediately unblock the organization if the new limit exceeds current spend. Requires the **OWNER** role. Alternatively, wait for the next billing period — the block expires automatically at the end of the current period. # subscription_already_canceled Source: https://docs.particle.pro/errors/subscription_already_canceled Subscription is already canceled — sign up again to reactivate | | | | ------------------ | ------------------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `subscription_already_canceled` | | **Resolve action** | `select_plan` | ## What happened `PUT /v1/organizations/{orgId}/billing/subscription` (change-plan) was rejected because the organization's cancellation has already taken effect — there is no active Orb subscription to modify. (For canceled-but-not-yet-effective state, see [`cancellation_revertable`](/errors/cancellation_revertable).) ## How to fix Sign up again with `POST /v1/organizations/{orgId}/billing/subscription`. A new Orb subscription is created on the same Orb customer using the plan ID supplied in the request body; previously-granted signup credit is preserved so it is not granted twice. # subscription_canceled Source: https://docs.particle.pro/errors/subscription_canceled Subscription has been canceled | | | | ------------------ | -------------------------------------------------------------------- | | **HTTP status** | `402 Payment Required` (data API) / `403 Forbidden` (org management) | | **Error code** | `subscription_canceled` | | **Resolve action** | `select_plan` | ## What happened The organization's subscription has been canceled and the cancellation has taken effect. PAYG cancellations take effect immediately; fixed-price cancellations take effect at the end of the current billing period. Once effective, API access is blocked and most org-management mutations (creating projects, API keys, invites, spend limits) are disabled. Canceled organizations remain readable — usage history, members, billing summary, and invoices stay accessible. ## How to fix Sign up again with `POST /v1/organizations/{orgId}/billing/subscription` to reactivate the organization on the same Orb customer. The signup credit is not granted twice (the `one_time_credit_granted` flag persists across cancellations). If the cancellation has *not* yet taken effect (you canceled a Growth subscription mid-period), call `POST /v1/organizations/{orgId}/billing/subscription/resume` instead to undo the scheduled cancellation without creating a new subscription — see [`cancellation_revertable`](/errors/cancellation_revertable). # subscription_inactive Source: https://docs.particle.pro/errors/subscription_inactive Subscription is not active | | | | ------------------ | ----------------------- | | **HTTP status** | `402 Payment Required` | | **Error code** | `subscription_inactive` | | **Resolve action** | `contact_support` | ## What happened The organization's subscription is in an inactive state. This can occur due to cancellation, administrative action, or an unrecognized subscription lifecycle state. ## How to fix Contact support at or visit the support page to restore your subscription. # subscription_not_canceled Source: https://docs.particle.pro/errors/subscription_not_canceled Subscription is not in the canceled state, so it cannot be resumed | | | | --------------- | --------------------------- | | **HTTP status** | `409 Conflict` | | **Error code** | `subscription_not_canceled` | ## What happened `POST /v1/organizations/{orgId}/billing/subscription/resume` was called on an organization whose subscription is not in the `CANCELED` state. The resume endpoint only reverts a scheduled cancellation — there is nothing to revert here. ## How to fix No action is needed: the subscription is already active. To switch plans use `PUT /v1/organizations/{orgId}/billing/subscription`; to cancel use `POST /v1/organizations/{orgId}/billing/cancel`. # subscription_not_found Source: https://docs.particle.pro/errors/subscription_not_found Billing subscription not found | | | | ------------------ | ------------------------- | | **HTTP status** | `503 Service Unavailable` | | **Error code** | `subscription_not_found` | | **Resolve action** | `contact_support` | ## What happened The organization has a plan set locally but no matching subscription could be found in the billing provider. This is an unexpected state that requires manual intervention. ## How to fix Contact support at or visit the support page to resolve your billing state. # subscription_suspended Source: https://docs.particle.pro/errors/subscription_suspended Subscription suspended due to non-payment | | | | ------------------ | ------------------------ | | **HTTP status** | `402 Payment Required` | | **Error code** | `subscription_suspended` | | **Resolve action** | `contact_support` | ## What happened The organization's subscription has been suspended after repeated payment failures. API access is fully blocked. ## How to fix Contact support at or visit the support page to restore your subscription. # token_expired Source: https://docs.particle.pro/errors/token_expired Reset token has expired or already been used | | | | ------------------ | ------------------------ | | **HTTP status** | `410 Gone` | | **Error code** | `token_expired` | | **Resolve action** | `request_password_reset` | ## What happened The password reset token has expired (tokens are valid for 1 hour) or has already been used to reset the password. ## How to fix Request a new password reset link to receive a fresh reset link in your email. # token_invalid Source: https://docs.particle.pro/errors/token_invalid Invalid or expired reset token | | | | ------------------ | ------------------------ | | **HTTP status** | `404 Not Found` | | **Error code** | `token_invalid` | | **Resolve action** | `request_password_reset` | ## What happened The password reset token is not recognized. It may have been mistyped, truncated, or belongs to a different environment. ## How to fix Request a new password reset link to receive a fresh reset link in your email. Reset tokens expire after 1 hour and can only be used once. # validation_error Source: https://docs.particle.pro/errors/validation_error Request body or parameters failed validation | | | | --------------- | -------------------------- | | **HTTP status** | `422 Unprocessable Entity` | | **Error code** | `validation_error` | ## What happened The request was well-formed but failed schema validation. One or more fields violated type, format, range, or presence constraints. The `errors` array contains one entry per field. Each includes a `location` (e.g. `body.items[3].name`) and a `message` describing the violation. ## How to fix Inspect the `errors` array, fix the offending fields, and retry. # Particle API Source: https://docs.particle.pro/index Inside every podcast: transcripts, speakers, entities, sponsors, and a knowledge graph that connects people and companies across episodes. Particle API gives you what's inside the audio. Every episode is transcribed and diarized, broken into structural segments, distilled into engagement-scored clips, and linked to a knowledge graph of people, organizations, and companies. The same identifiers connect a guest on one podcast to a company's full profile, its product hierarchy, and the sponsors funding the shows where it's discussed. Use it to track what people are saying, find the most quotable moments on any subject, monitor how companies show up across audio, or build research tools that read audio the way they read text. Make your first call and trace one entity across podcasts, dialogue, and companies in five minutes. Episodes, transcripts, segments, clips, and the bias analysis behind every show. Cross-referenced identifiers (CIK, ticker, domain, QID, entity slug) and full product hierarchies. People, organizations, and places — the connective tissue between audio and structured data. Sponsor breakdowns, leaderboards, co-occurrence, and per-company ad presence. Every endpoint, request, and response schema. # Entities Source: https://docs.particle.pro/knowledge-graph/entities People, organizations, places, and concepts — the connective tissue across audio and structured data. Entities are the knowledge-graph layer that connects everything else. The same entity (Sam Altman, Nvidia, Federal Reserve) appears as a speaker on episodes, as a mention in transcripts, as a sponsor in advertising data, and as the linked identity on a company profile. Resolve once, then follow the entity across the platform. Each entity has a stable `slug` (e.g. `sam-altman`, `nvidia`, `kara-swisher`) and a canonical `id`. Either is accepted anywhere an entity reference is needed. Available to MCP agents as [`particle_entity_resolve`](/mcp/tools/people/entity-resolve) (the union resolver) and [`particle_person_resolve`](/mcp/tools/people/person-resolve) (people only). ## Search entities ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/entities?q=Sam+Altman&limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/entities?q=Sam+Altman&limit=3", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/entities", params={"q": "Sam Altman", "limit": 3}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "17PzxG1t12xzno", "slug": "sam-altman", "name": "Sam Altman", "description": "CEO of OpenAI", "wikipedia_url": "https://en.wikipedia.org/wiki/Sam_Altman" } ], "has_more": false } ``` ### Filter parameters | Parameter | Description | | ------------ | ----------------------------------------------------------------------------------------------------- | | `q` | Name search (case-insensitive) | | `type` | Restrict to an entity category (e.g. `person`, `company`); see `GET /v1/entities/types` for the slugs | | `podcast_id` | Restrict to entities appearing in a specific podcast | | `ids` | Bulk multi-get: fetch a known set of entities by slug or ID in one call (see below) | ## Get a single entity ```bash theme={"dark"} # Either of these resolves to Sam Altman curl "https://api.particle.pro/v1/entities/sam-altman" \ -H "X-API-Key: $PARTICLE_API_KEY" curl "https://api.particle.pro/v1/entities/17PzxG1t12xzno" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response theme={"dark"} { "id": "17PzxG1t12xzno", "slug": "sam-altman", "name": "Sam Altman", "description": "CEO of OpenAI", "wikipedia_url": "https://en.wikipedia.org/wiki/Sam_Altman", "image_url": "https://cdn.particle.pro/url/media/kge/m/02kx06l/…" } ``` ## Fetch many at once When you already hold a set of entity slugs or IDs, pass them as a comma-separated `ids` list to fetch them all in a single request instead of one call per entity — up to 100 per call. Entities come back in the same shape as search and in the order you asked for them. A ref that doesn't resolve is simply left out (no error, no placeholder), so compare the returned `slug`/`id` values against what you sent to find any that are missing. ```bash theme={"dark"} curl "https://api.particle.pro/v1/entities?ids=sam-altman,nvidia,17PzxG1t12xzno" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "17PzxG1t12xzno", "slug": "sam-altman", "name": "Sam Altman" }, { "id": "2dFq…", "slug": "nvidia", "name": "Nvidia" } ], "has_more": false } ``` When `ids` is present the other filters (`q`, `type`, `podcast_id`) and pagination are ignored — it is a direct multi-get of exactly the refs you supply. ## Podcast appearances To find every podcast episode where an entity has been featured or discussed across the catalog, use [`list-episodes`](/podcasts/episodes#list-episodes) with an `entity_id` filter. Add a `role` filter to narrow to `guest`, `host`, `panelist`, `correspondent`, or `mention`. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes?entity_id=sam-altman&limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "78cgekLUjCJBUZbj3s5K8Y", "title": "WHCD Shooting Aftermath, Musk and Altman Face-Off, Spirit Airlines Bailout", "podcast": { "id": "QpMz7GYKfSNuUa6zKXA4Q", "title": "Pivot" }, "published_at": "2026-04-28T10:00:00Z", "duration_seconds": 4206, "has_transcript": true, "segment_count": 21, "clip_count": 8, "entity_count": 146, "speakers": [ /* … */ ] } // … ], "has_more": true, "cursor": "…" } ``` Add `role=guest` to limit results to episodes where the entity was actually a guest, rather than every episode that happens to mention them. ## A typical cross-referencing flow Look up by name (`?q=`) or use a slug you already have. ```bash theme={"dark"} curl ".../v1/entities/sam-altman" ``` Use the slug as `entity_id` on episodes or clips. ```bash theme={"dark"} curl ".../v1/podcasts/episodes?entity_id=sam-altman&limit=10" ``` Pull the dialogue lines around mentions, or the highlight clips. ```bash theme={"dark"} curl ".../v1/podcasts/episodes/{id}/transcript/mentions?entity_id=sam-altman" curl ".../v1/podcasts/episodes/{id}/clips" ``` Many entities are also linked to companies. Lookup by `entity_id` on the companies endpoint. ```bash theme={"dark"} curl ".../v1/companies?entity_id=nvidia" ``` The [Quickstart](/quickstart) walks the full flow end-to-end. ## Related * [Topics](/knowledge-graph/topics) — taxonomy companion to entities * [Companies → Overview](/companies/overview) — entities linked to company records * [Transcripts → Mentions](/podcasts/transcripts#transcript-mentions) — dialogue around entity mentions * [Concepts → IDs and slugs](/concepts#ids-and-slugs) — slug resolution rules # Topics Source: https://docs.particle.pro/knowledge-graph/topics Hierarchical topic taxonomy used to classify episodes. Topics form a tree. Broad categories like `Politics` and `Business` contain subtopics like `politics/elections` and `business/technology`. Episodes are classified into topics automatically — fetch the taxonomy to power category navigation, route episodes to topic-specific feeds, or filter content by interest. Available to MCP agents as [`particle_topic_browse`](/mcp/tools/topics/topic-browse). ## Browse the taxonomy Top-level topics: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/topics?limit=10" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch("https://api.particle.pro/v1/topics?limit=10", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); const { data } = await res.json(); ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "FMABRApV", "name": "Society", "slug": "society", "ancestry_path": "E-sJ7" }, { "id": "4FZ5KFM", "name": "Government", "slug": "government", "ancestry_path": "Qcqm_" }, { "id": "4FaC4H9", "name": "Business", "slug": "business", "ancestry_path": "gWDfW" }, { "id": "4FZ4EFh", "name": "Politics", "slug": "politics", "ancestry_path": "GtizK" }, { "id": "4K6dD5X", "name": "Media", "slug": "media", "ancestry_path": "cbYxV" } // … ] } ``` The `ancestry_path` is an opaque hash-based path used for programmatic ancestor filtering. Slugs follow a `parent/child` convention for subtopics (e.g. `politics/elections`). ## Navigate the tree Get direct children of a topic by passing `parent_id`: ```bash theme={"dark"} # Subtopics under Politics curl "https://api.particle.pro/v1/topics?parent_id=4FZ4EFh" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "4FZ4EFi", "name": "International Relations", "slug": "politics/international-relations", "ancestry": "Politics" }, { "id": "4FZ53X4", "name": "Elections", "slug": "politics/elections", "ancestry": "Politics" }, { "id": "FMEs52mJ", "name": "Political Parties", "slug": "politics/political-parties", "ancestry": "Politics" }, { "id": "GIO2VHkb", "name": "Political Figures", "slug": "politics/political-figures", "ancestry": "Politics" } // … ] } ``` ## Get a single topic The topic detail endpoint accepts any of three identifier forms in the path: * The encoded `id` returned by the list endpoint (e.g. `4FZ4EFh`) * The full `slug`, including multi-level slugs with `/` (e.g. `politics/elections`) * The `ancestry_path` hash (e.g. `GtizK`, `Qcqm_`) ```bash theme={"dark"} curl "https://api.particle.pro/v1/topics/4FZ4EFh" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response theme={"dark"} { "id": "4FZ4EFh", "name": "Politics", "slug": "politics", "ancestry_path": "GtizK", "total_children": 27601, "children": [ { "id": "4FZ4EFi", "name": "International Relations", "slug": "politics/international-relations", "ancestry": "Politics", "ancestry_path": "GtizKFabWL" }, { "id": "4FZ53X4", "name": "Elections", "slug": "politics/elections", "ancestry": "Politics", "ancestry_path": "GtizKFGV8r" } // … up to 100 entries, ordered by prominence ] } ``` Response fields: * `ancestors` — array of topic names from root to parent. Absent for root topics. * `ancestry` — single human-readable breadcrumb string. Absent for root topics. * `total_children` — total number of direct child topics. Always present (0 for leaves). * `children` — the top direct children ordered by prominence (`url_cluster_count` descending, ties broken by name). Capped at 100; when `total_children` exceeds 100, paginate the full set via `/v1/topics?parent_id={id}`. ## Topics for an episode Podcast episodes are classified into topics automatically. Fetch the topics for any episode: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/{episode_id}/topics" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` See [Episodes → Topics](/podcasts/episodes#topics) for the integrated discovery flow. ## Related * [Entities](/knowledge-graph/entities) — the people, organizations, and places counterpart to topics * [Episodes](/podcasts/episodes) — filter by topic, then drill into transcripts and clips # Authentication Source: https://docs.particle.pro/mcp/authentication OAuth 2.1 for interactive clients — discovery, dynamic client registration, PKCE, refresh, revocation — and API-key bearer auth for headless agents. The Particle Pro MCP server accepts two credentials, matched to two kinds of callers: * **OAuth 2.1** (interactive clients — Claude Code, Cursor, VS Code, ChatGPT, Claude Desktop): a user approves the connection in a browser once, and every request thereafter carries a short-lived JWT minted by the Particle Pro Authorization Server, bound to the MCP resource via its `aud` claim. Granular, per-project, user-revocable — prefer it whenever a human is present to consent. * **Platform API keys** (headless agents — cron jobs, server-side harnesses, [OpenAI's Responses API](https://platform.openai.com/docs/guides/tools-remote-mcp#authentication)): the same `pp_*` keys the REST API accepts, sent as a bearer. No browser, no consent screen — see [API keys for headless agents](#api-keys-for-headless-agents). Most of this page documents the OAuth flow for client implementers. If you are using a stock MCP client the flow is automatic — see the [Quickstart](/mcp/quickstart). ## At a glance | | | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | **Protected resource** | `https://mcp.particle.pro` | | **Resource metadata** | `https://mcp.particle.pro/.well-known/oauth-protected-resource` (and `/mcp` sub-path) | | **Authorization Server** | `https://api.particle.pro` | | **AS metadata** | `https://api.particle.pro/.well-known/oauth-authorization-server` | | **JWKS** | `https://api.particle.pro/.well-known/jwks.json` | | **Grant types** | `authorization_code`, `refresh_token` | | **PKCE** | Required, `code_challenge_method=S256` only | | **Auth methods** | `none` (public clients), `client_secret_basic`, `client_secret_post` | | **Scopes** | `mcp:read`, `mcp:write` | | **Access token TTL** | 15 minutes | | **Refresh token TTL** | 30 days, rotated on every use | | **Agent onboarding** | [`https://api.particle.pro/auth.md`](https://api.particle.pro/auth.md) (also on the MCP host) + `agent_auth` block in the AS metadata | ## Discovery Start from the resource. Fetch the protected-resource metadata document: ```http theme={"dark"} GET /.well-known/oauth-protected-resource HTTP/1.1 Host: mcp.particle.pro ``` ```json theme={"dark"} { "resource": "https://mcp.particle.pro", "authorization_servers": ["https://api.particle.pro"], "bearer_methods_supported": ["header"], "scopes_supported": ["mcp:read", "mcp:write"], "resource_documentation": "https://docs.particle.pro/mcp" } ``` Then fetch the Authorization Server metadata from the URL in `authorization_servers`: ```http theme={"dark"} GET /.well-known/oauth-authorization-server HTTP/1.1 Host: api.particle.pro ``` ```json theme={"dark"} { "issuer": "https://api.particle.pro", "authorization_endpoint": "https://api.particle.pro/oauth/authorize", "token_endpoint": "https://api.particle.pro/oauth/token", "registration_endpoint": "https://api.particle.pro/oauth/register", "revocation_endpoint": "https://api.particle.pro/oauth/revoke", "jwks_uri": "https://api.particle.pro/.well-known/jwks.json", "scopes_supported": ["mcp:read", "mcp:write"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"], "subject_types_supported": ["public"], "client_id_metadata_document_supported": true, "service_documentation": "https://docs.particle.pro/mcp/oauth" } ``` The live document also carries an `agent_auth` block for autonomous agents — see [Agent discovery](#agent-discovery) below. If a request to the MCP endpoint comes in unauthenticated, the server returns `401` with a `WWW-Authenticate` header pointing at the resource-metadata document, so a client that doesn't pre-fetch metadata still discovers the AS: ```http theme={"dark"} HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="mcp", error="invalid_token", resource_metadata="https://mcp.particle.pro/.well-known/oauth-protected-resource" ``` The human-readable description (e.g. `bearer token required`, `grant revoked or unknown`) is in the response body, not the header. ## Agent discovery Particle Pro publishes the [auth.md](https://github.com/workos/auth.md) agent-registration protocol, so autonomous agents (and agent-readiness scanners) can discover how to obtain a credential without reading human documentation: * **`/auth.md`** — a human- and machine-readable onboarding recipe at [`https://api.particle.pro/auth.md`](https://api.particle.pro/auth.md) and [`https://mcp.particle.pro/auth.md`](https://mcp.particle.pro/auth.md): the discovery walkthrough, a credential decision tree, registration steps, errors, and revocation. A docs-flavored mirror lives at [Agent authentication](/auth). * **`agent_auth`** — a machine-readable block inside the AS metadata document above: ```json theme={"dark"} "agent_auth": { "skill": "https://api.particle.pro/auth.md", "register_uri": "https://api.particle.pro/oauth/register", "revocation_uri": "https://api.particle.pro/oauth/revoke", "authorization_endpoint": "https://api.particle.pro/oauth/authorize", "token_endpoint": "https://api.particle.pro/oauth/token", "identity_types_supported": ["user_consent_authorization_code"], "credential_types_supported": ["access_token", "api_key"], "user_consent_authorization_code": { "registration_endpoint": "https://api.particle.pro/oauth/register", "authorization_endpoint": "https://api.particle.pro/oauth/authorize", "token_endpoint": "https://api.particle.pro/oauth/token", "code_challenge_methods_supported": ["S256"], "resource": "https://mcp.particle.pro", "consent_uri": "https://platform.particle.pro/oauth/consent" }, "unsupported_flows": { "anonymous": "No anonymous or self-service agent credential issuance. Every credential is tied to a human-owned platform account.", "identity_assertion": "No ID-JAG / identity-assertion exchange. Use authorization_code + PKCE (interactive) or a dashboard-issued pp_* API key (headless).", "device_authorization": "No device authorization grant. Headless agents use a pp_* API key minted at https://platform.particle.pro/tokens." }, "events_supported": [] } ``` Every advertised URL is a shipping endpoint. The only identity type is `user_consent_authorization_code` — [dynamic client registration](#dynamic-client-registration-rfc-7591) (or a [Client ID Metadata Document](#client-id-metadata-document-cimd)) followed by the [PKCE authorization-code flow](#authorization-code-with-pkce), with a human approving on platform.particle.pro. Flows Particle Pro deliberately does not offer (anonymous registration, ID-JAG identity assertion, device authorization) are declared in `unsupported_flows`, so an agent fails fast instead of probing. The REST API publishes its own RFC 9728 document at `https://api.particle.pro/.well-known/oauth-protected-resource`. Mind the asymmetry it implies: the REST surface authenticates with `pp_*` API keys only — AS-issued access tokens are audience-bound to the MCP resource and are not accepted by the REST API. ## Client registration You have two ways to register a client. ### Dynamic Client Registration (RFC 7591) The simplest path. POST to the registration endpoint with at least `client_name` and `redirect_uris`: ```http theme={"dark"} POST /oauth/register HTTP/1.1 Host: api.particle.pro Content-Type: application/json { "client_name": "Acme Research Agent", "client_uri": "https://acme.example", "logo_uri": "https://acme.example/logo.png", "redirect_uris": ["http://localhost:43217/callback"], "token_endpoint_auth_method": "none" } ``` Response (RFC 7591 § 3.2): ```json theme={"dark"} { "client_id": "c_abc123…", "client_name": "Acme Research Agent", "redirect_uris": ["http://localhost:43217/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" } ``` Defaults applied when omitted: * `token_endpoint_auth_method`: `none` (i.e. public client, PKCE-only) * `grant_types`: `["authorization_code", "refresh_token"]` * `response_types`: `["code"]` For confidential clients (any `token_endpoint_auth_method` other than `none`) the response includes `client_secret` exactly once — store it securely, the AS does not return it again. ### Client ID Metadata Document (CIMD) Agents that already publish a metadata document at an HTTPS URL can skip DCR entirely and pass the URL itself as `client_id` to `/oauth/authorize`. The AS fetches the document, validates it, and caches it. The CIMD spec is `draft-ietf-oauth-client-id-metadata-document`. Use this when your agent runs in many places (you don't want to register N clients) but has a stable identity (your domain). ## Authorization code with PKCE Generate a high-entropy `code_verifier`, then derive its S256 challenge: ```bash theme={"dark"} code_verifier=$(openssl rand -base64 64 | tr -d '\n' | tr '+/' '-_' | tr -d '=' | cut -c1-128) code_challenge=$(printf "%s" "$code_verifier" | openssl dgst -sha256 -binary | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=') ``` Open the authorization URL in a browser: ``` https://api.particle.pro/oauth/authorize? response_type=code& client_id=c_abc123…& redirect_uri=http%3A%2F%2Flocalhost%3A43217%2Fcallback& scope=mcp%3Aread%20mcp%3Awrite& state=& code_challenge=& code_challenge_method=S256& resource=https%3A%2F%2Fmcp.particle.pro ``` Required parameters: * `response_type=code` (only flow supported) * `client_id` — from registration or CIMD URL * `redirect_uri` — must match a value the client registered * `code_challenge` + `code_challenge_method=S256` — PKCE is mandatory * `resource=https://mcp.particle.pro` — RFC 8707 audience binding; the AS rejects any other value `scope` is optional. If omitted the AS issues `mcp:read mcp:write` by default. Naming an unknown scope is rejected (the AS will redirect with `error=invalid_scope` rather than silently up-scoping). The user signs in (if not already), picks a project, and approves the requested scopes. The AS redirects back to your `redirect_uri`: ``` http://localhost:43217/callback?code=auth_xyz…&state= ``` Exchange the code for tokens: ```http theme={"dark"} POST /oauth/token HTTP/1.1 Host: api.particle.pro Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code=auth_xyz…& redirect_uri=http%3A%2F%2Flocalhost%3A43217%2Fcallback& code_verifier=& client_id=c_abc123…& resource=https%3A%2F%2Fmcp.particle.pro ``` For confidential clients, authenticate one of two ways: * **`client_secret_basic`** — drop `client_id` from the form body and send `client_id:client_secret` URL-form-encoded in an HTTP Basic `Authorization` header. * **`client_secret_post`** — keep `client_id` in the form body and add a `client_secret` field alongside it. Public clients (`token_endpoint_auth_method=none`) keep `client_id` in the form body and send no secret. Response: ```json theme={"dark"} { "access_token": "eyJhbGciOiJSUzI1NiIs…", "token_type": "Bearer", "expires_in": 900, "refresh_token": "rt_…", "scope": "mcp:read mcp:write" } ``` ## Calling the MCP endpoint Send the access token as a bearer in the `Authorization` header. The MCP server only accepts header-borne tokens (`bearer_methods_supported: ["header"]`) — no query-string tokens, no form-body tokens. ```http theme={"dark"} POST /mcp HTTP/1.1 Host: mcp.particle.pro Authorization: Bearer eyJhbGciOiJSUzI1NiIs… Content-Type: application/json { "jsonrpc": "2.0", "method": "tools/list", "id": 1 } ``` The resource server validates the token in this order: 1. Verify the JWT signature against the rotating JWKS at `https://api.particle.pro/.well-known/jwks.json`. 2. Confirm `iss=https://api.particle.pro`, `aud=https://mcp.particle.pro`, and `exp` is in the future. 3. Look up the `grant_id` claim — the grant must still be active (not revoked, not user-disabled). 4. Confirm the token's `sub`, `client_id`, and `project_id` claims match the grant row (defense in depth against a buggy or compromised signer). Any failure returns `401` with a `WWW-Authenticate` challenge. ## API keys for headless agents Autonomous agents have no browser to complete an OAuth consent in. For them, the MCP endpoint accepts the same project-scoped `pp_*` API keys as the REST API — as an `Authorization: Bearer` header or an `X-API-Key` header: ```bash theme={"dark"} curl https://mcp.particle.pro/ \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer $PARTICLE_API_KEY" \ -d '{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "particle_entity_resolve", "arguments": { "query": "sam altman" } }, "id": 1 }' ``` The key authenticates exactly as it does on the REST surface — same project and organization binding, same subscription gating, same rate limits, and tool calls meter identically to OAuth traffic. Create and rotate keys from [platform.particle.pro](https://platform.particle.pro) → your project's settings. Credential hygiene for agents: * **Keys are project-scoped** — give each agent its own key in a project provisioned for it, so a leaked key exposes one project's access, is attributable in request logs, and can be revoked without breaking anything else. * **Never embed keys in code or prompts.** Provide them through a secrets vault or environment variable, as in the `$PARTICLE_API_KEY` example above. * **Prefer OAuth when a human is present.** Consent-scoped, short-lived, per-client tokens beat a long-lived shared secret anywhere an interactive approval is possible. An invalid or expired key gets a `401` whose body says `invalid or expired API key` — distinct from the JWT validation errors above, so a misconfigured agent can tell which credential path failed. ## Refresh and rotation When the access token nears expiry, exchange the refresh token: ```http theme={"dark"} POST /oauth/token HTTP/1.1 Host: api.particle.pro Content-Type: application/x-www-form-urlencoded grant_type=refresh_token& refresh_token=rt_…& client_id=c_abc123…& resource=https%3A%2F%2Fmcp.particle.pro ``` Refresh tokens **rotate** on every use — the response carries a *new* refresh token; the old one is consumed. If the AS sees the same refresh token presented twice (replay) or two refreshes race for the same token, it revokes the entire grant chain immediately. Store the latest refresh token atomically; never retry a refresh request with the same token. ## Revocation Either side can revoke a grant: ```http theme={"dark"} POST /oauth/revoke HTTP/1.1 Host: api.particle.pro Content-Type: application/x-www-form-urlencoded token=rt_…& client_id=c_abc123… ``` Users can also revoke connections from the platform UI — see [Manage connections](#manage-connections) below. Once a grant is revoked, all access tokens issued from it stop validating immediately at the resource server (the grant lookup fails) and the refresh token can no longer be exchanged. ## Manage connections Every OAuth approval creates a **connection** (a grant) binding one MCP client to one of your projects. You can review and revoke them at any time — no client involvement needed: 1. Sign in at [platform.particle.pro](https://platform.particle.pro) and open **Connected Applications**. 2. Each entry shows the client's registered name, the project it acts on, and when it was approved. Find the client you want to disconnect. 3. Choose **Revoke**. The grant — and every access and refresh token issued from it — stops working immediately. To reconnect, just use the client again: the next tool call triggers a fresh OAuth flow, and the consent screen lets you pick a different project if the original grant was scoped to the wrong one (the most common reason for a `403` after a previously working connection). Revoke a connection whenever a device is lost, an agent misbehaves, or you no longer use the client — reconnecting later is a one-click approval. ## Audience binding (RFC 8707) Every authorization request and every token request must include `resource=https://mcp.particle.pro`. The AS rejects mismatches with `error=invalid_target`. The minted access token carries `aud=https://mcp.particle.pro`, and the MCP server rejects any token with a different `aud` — even a valid token issued for some hypothetical other Particle resource would not work here. This is the standard "confused deputy" safeguard. ## Scopes Two scopes today; finer-grained scopes can be added as the surface grows: | Scope | Granted access | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `mcp:read` | Read your Particle Pro data via MCP tools. | | `mcp:write` | Take actions on your behalf in Particle Pro via MCP tools. (All current tools are read-only — `mcp:write` is reserved for future write tools.) | Today every tool advertises `readOnlyHint: true`; `mcp:write` is requested by default but does not unlock additional surface yet. ## Key rotation The AS uses RS256 with a rotating JWKS. The signer publishes the "next" key well before promoting it to "active" and keeps "retired" keys live long enough for outstanding tokens to expire (15-minute access-token TTL). Validators cache the JWKS for up to 5 minutes — a fresh `kid` will be picked up within that window. ## See also * [Quickstart](/mcp/quickstart) — install snippets for stock MCP clients. * [Errors](/mcp/errors) — how tool errors surface (different from REST `application/problem+json`). * [`auth_required`](/errors/auth_required) — REST-side auth error code. # Changelog Source: https://docs.particle.pro/mcp/changelog What's new on the Particle Pro MCP server — new tools and capabilities, and anything that changes how your agent connects or calls. Tool *behavior* grows additively — new `include` sections and modes never break an existing call. Renames and changes that could affect your integration are called out explicitly. ## June 2026 ### Richer external links * **External-link attributes in markdown** — when [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) hydrates `include: ["external_links"]`, each platform bullet now appends the attributes the platform reports (audience size, content tallies, verified status, ...), so a YouTube channel renders its subscriber and video counts alongside the URL rather than the link alone. The attributes were already present on the `output_format: "json"` form; this surfaces them in the default markdown. ### Agent discovery (auth.md) * **`/auth.md`** — both hosts now serve the [auth.md](https://github.com/workos/auth.md) agent-onboarding document at [`https://api.particle.pro/auth.md`](https://api.particle.pro/auth.md) and [`https://mcp.particle.pro/auth.md`](https://mcp.particle.pro/auth.md): the discovery walkthrough, a credential decision tree, registration steps, errors, and revocation, with the flows Particle Pro deliberately does not offer declared up front. * **`agent_auth` block** — the [AS metadata](https://api.particle.pro/.well-known/oauth-authorization-server) now embeds a machine-readable registration summary (`register_uri`, identity and credential types, revocation). [Authentication → Agent discovery](/mcp/authentication#agent-discovery). * **REST protected-resource metadata** — `https://api.particle.pro/.well-known/oauth-protected-resource` now serves an RFC 9728 document describing the REST API resource (previously 404). The MCP document at `https://mcp.particle.pro/.well-known/oauth-protected-resource` is unchanged. ### Skill bundles and exact-tool selection * **[Skill bundles](/mcp/skills)** — three ready-made skills (Podcast Intelligence, Company Deep Dive, Guest & People Research): a connection URL scoped to just the tools each workflow needs, plus a copy-paste SKILL.md that teaches your agent the workflow. * **`?tools=` selector** — pin a connection to exactly the tools you want advertised: `?tools=particle_podcast_resolve,particle_podcast_search_transcripts`. See [Tool sets & discovery](/mcp/tool-sets#selecting-exact-tools). * **More clients in the quickstart** — Windsurf, Zed, ChatGPT developer mode, and server-side wiring examples for the Anthropic Messages API and OpenAI Responses API. [Quickstart](/mcp/quickstart). ### Connect without a browser * **API keys as MCP credentials** — the server now accepts the same `pp_*` project API keys as the REST API (`Authorization: Bearer` or `X-API-Key`), so headless agents — cron jobs, the OpenAI Responses API, server-side harnesses — no longer need an interactive OAuth consent. OAuth remains the recommended path when a human is present. [Authentication](/mcp/authentication#api-keys-for-headless-agents). * **One-click installs** — Cursor and VS Code install buttons in the [Quickstart](/mcp/quickstart). * **[llms.txt](https://docs.particle.pro/llms.txt)** — now opens with instructions for LLM agents: which surface to pick, how the tools compose, and the conventions that save wasted round-trips. ### Smarter discovery on connect * **Conventions taught up front** — connecting clients now receive server instructions covering the two things a bare tool list doesn't show: tools are lean by default and expand via `include`/`mode`/`format` parameters, and every slug a tool returns is a valid input to the other tools (resolve a name once, then traverse). * **`particle_catalog` shows each tool's depth** — the catalog menu lists every tool's expand options (`↳ include: … · mode: …`), so an agent can see what a tool can really do before fetching its full schema. ### 25 tools, flat names, opt-in categories * **Renames (action needed if you call tools by name)** — every tool moved to a flat `particle_*` identifier, e.g. `entities/resolve_entity` → `particle_entity_resolve`. The [tool reference](/mcp/tools/overview) lists all current names. * **Consolidations** — `search_dialogue` and `list_clips` merged into [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts): relevant clips now arrive inline on search matches, and a known episode's full clip list comes from `particle_podcast_get_episode` with `include: ["clips"]`. `get_entity_mentions` became [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions). Boolean flags on get-tools became `include` arrays. * **12 new tools** — people profiles and resolution, podcast guests, chart rankings, publishers, listener ratings, and political-bias and brand-suitability analytics. Browse them in the [tool reference](/mcp/tools/overview). * **Default vs opt-in advertisement** — six tool categories advertise on a bare connection; five more (advertising, publishers, ratings, bias, suitability) advertise when you opt in with `?include=`. Every public tool stays callable by name either way, and the always-on `particle_catalog` / `particle_call` meta-tools let any connection discover and reach the full surface. [Tool sets & discovery](/mcp/tool-sets). ## May 2026 ### Launch * The Particle Pro MCP server ships at `https://mcp.particle.pro`: read-only tools over podcasts, companies, and the knowledge graph, with [OAuth 2.1](/mcp/authentication) and markdown outputs designed for agent loops. * MCP requests and per-tool usage appear in your organization's request logs alongside REST traffic. *** Missing something? Email [api@particle.pro](mailto:api@particle.pro). # Errors Source: https://docs.particle.pro/mcp/errors How tool errors surface in MCP responses, and how the REST error catalog maps onto them. The MCP server surfaces three distinct kinds of failures, each with a different shape. This page explains how to recognize and react to each one. ## 1. Authentication / authorization failures (HTTP) If the bearer token is missing, malformed, expired, or revoked, the MCP endpoint returns an HTTP `401 Unauthorized` *before* the request reaches the tool layer. The response carries an RFC 9728 `WWW-Authenticate` challenge: ```http theme={"dark"} HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="mcp", error="invalid_token", resource_metadata="https://mcp.particle.pro/.well-known/oauth-protected-resource" Content-Type: text/plain grant revoked or unknown ``` The human-readable explanation lives in the response body. The header carries only the structured fields (`realm`, `error`, optional `scope`, `resource_metadata`). Reactions: * Re-run the OAuth flow. The `resource_metadata` URL is the authoritative pointer to the AS — do not hard-code the token endpoint. * For revoked grants, the user must reconnect — see [Manage connections](/mcp/authentication#manage-connections). * If the request authenticated with a [platform API key](/mcp/authentication#api-keys-for-headless-agents) instead of OAuth, a bad key gets the same `401` shape with the body `invalid or expired API key` — rotate or re-issue the key from the project settings; the OAuth challenge in the header does not apply to you. See [Authentication](/mcp/authentication) for the full validation pipeline. ## 2. Subscription / rate-limit failures (HTTP) Requests that pass authentication but hit a subscription block or per-org rate limit return RFC 9457 `application/problem+json` bodies — the same shape as the REST API. The response is HTTP, not an MCP tool result, because the request never reached a tool. ```http theme={"dark"} HTTP/1.1 402 Payment Required Content-Type: application/problem+json { "type": "https://docs.particle.pro/errors/spend_limit_exceeded", "title": "Payment Required", "status": 402, "detail": "Spend limit reached for the current billing period.", "error_code": "spend_limit_exceeded", "resolve": { "message": "Increase the spend limit or upgrade the plan to continue.", "url": "https://platform.particle.pro/organizations//billing", "action": "update_spend_limits", "method": "POST", "endpoint": "/v1/organizations//billing/spend-limits" } } ``` The full set of codes is documented in [REST → Errors](/errors/overview). The ones an MCP client is most likely to see at this layer: * [`spend_limit_exceeded`](/errors/spend_limit_exceeded), [`credits_depleted`](/errors/credits_depleted), [`rate_limit_exceeded`](/errors/rate_limit_exceeded) * [`subscription_suspended`](/errors/subscription_suspended), [`payment_past_due`](/errors/payment_past_due), [`subscription_inactive`](/errors/subscription_inactive) * [`pro_required`](/errors/pro_required), [`not_a_member`](/errors/not_a_member) Branch on `error_code`; surface `resolve.message` and `resolve.url` to the user. Agents that can take action programmatically should use `resolve.action`, `resolve.method`, and `resolve.endpoint`. ## 3. Tool-level failures (MCP `isError: true`) When a request reaches a tool but the tool itself fails (missing required parameter, slug not found, invalid date, downstream handler error), the MCP server returns a `200 OK` HTTP response carrying an MCP `CallToolResult` with `isError: true` and a single text content block: ```json theme={"dark"} { "jsonrpc": "2.0", "id": 17, "result": { "isError": true, "content": [ { "type": "text", "text": "**Error code:** missing_parameter\n\ntool particle_entity_resolve requires parameter `query` (e.g. query=\"sam altman\")" } ] } } ``` Every tool error leads with a `**Error code:** ` line — a stable, machine-readable code to branch on without substring-matching the prose that follows. Two kinds of codes appear: * **MCP-only codes** for failures that never reach the REST layer: `missing_parameter`, `invalid_parameter`, `invalid_input` (malformed arguments), and `unknown_tool` (a `particle_call` dispatch to a name that doesn't exist on the public surface). * **REST catalog codes** reused as-is when the underlying handler fails — e.g. `not_found`, `bad_request`, `premium_required`. See [REST → Errors](/errors/overview) for the full catalog. After the code line comes the handler's message, and — where the server knows a better next move than a blind retry — a course-correcting suggestion. Structured `resolve` guidance from the REST layer (message + URL) is folded into that suggestion; common statuses get a default: * **404** suggests re-resolving the identifier first (`particle_podcast_resolve`, `particle_person_resolve`, `particle_company_resolve`, or `particle_entity_resolve`) and retrying with the returned slug. * **400/422** suggests checking the parameter constraints via [`particle_catalog`](/mcp/tools/system/catalog) with the tool's category, which returns the full input schema. Reactions: * Branch on the `**Error code:**` slug, then follow the suggestion in the text — it names the exact tool to call next. * For `missing_parameter` / `invalid_parameter`, the text includes a usage hint (e.g. `query="sam altman"`) — fix the call and retry. * For `internal_error`, surface the message to the user and avoid retry loops — the resource server logs the underlying error with full structure for support follow-up. ## Why three layers? * **HTTP 401** is the only path that's defined by RFC 6750 / RFC 9728 — MCP clients that follow the OAuth spec already handle it. Putting auth errors anywhere else would break the discovery loop. * **HTTP 402/403/429** with RFC 9457 bodies stays consistent with the REST API: the same billing or rate-limit middleware runs in front of both surfaces, so the same problem-details shape comes out. * **`isError: true` MCP results** are how the spec recommends per-tool failures be returned — a `200 OK` JSON-RPC response with the structured error in the result envelope, so the agent can keep the session open and fix the call. ## See also * [REST → Errors](/errors/overview) — the canonical catalog of `error_code` values and resolution actions. * [Authentication](/mcp/authentication) — how 401s arise and what `WWW-Authenticate` carries. * [Tool reference](/mcp/tools/overview) — every tool documents its required parameters and the failure modes it can raise. # Particle Pro MCP server Source: https://docs.particle.pro/mcp/overview An OAuth-secured MCP server that exposes Particle's podcast, knowledge graph, company, and advertising surfaces as tool calls for AI agents. The Particle Pro [MCP](https://modelcontextprotocol.io) server lives at **`https://mcp.particle.pro`** and lets AI agents — Claude Code, Claude Desktop, Cursor, ChatGPT Desktop, and any other MCP-aware client — call Particle's podcast, knowledge-graph, company, and advertising tools directly. Setup is a one-time browser approval; everything else is automated by the agent. ## Connect your agent **One-click install:** [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=particle\&config=eyJ1cmwiOiJodHRwczovL21jcC5wYXJ0aWNsZS5wcm8ifQ%3D%3D) · [Add to VS Code](https://vscode.dev/redirect/mcp/install?name=particle\&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.particle.pro%22%7D) — or use the matching manual config below. ```bash Claude Code theme={"dark"} claude mcp add --transport http particle https://mcp.particle.pro ``` ```json Claude Desktop theme={"dark"} // ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) // %APPDATA%\Claude\claude_desktop_config.json (Windows) // // Claude Desktop's claude_desktop_config.json doesn't accept a bare `url`. // Bridge the remote server through `mcp-remote`, which opens a browser // for OAuth and proxies stdio ↔ Streamable HTTP. Restart Claude Desktop // after editing. { "mcpServers": { "particle": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.particle.pro"] } } } ``` ```json Cursor theme={"dark"} // ~/.cursor/mcp.json { "mcpServers": { "particle": { "url": "https://mcp.particle.pro" } } } ``` ```json VS Code theme={"dark"} // .vscode/mcp.json { "servers": { "particle": { "type": "http", "url": "https://mcp.particle.pro" } } } ``` ```bash mcp-remote (any stdio-only client) theme={"dark"} # Bridges a stdio MCP client to a remote OAuth-secured Streamable HTTP server. npx -y mcp-remote https://mcp.particle.pro ``` **Claude Desktop UI alternative:** Settings → Connectors → **Add custom connector**, paste `https://mcp.particle.pro`, complete OAuth in the browser. Easier than editing JSON and avoids the `mcp-remote` bridge. **ChatGPT:** Settings → **Apps** → Advanced settings → enable **Developer mode** (requires a Pro, Plus, Business, Enterprise, or Edu plan) → **Create app** with `https://mcp.particle.pro` and complete OAuth in the browser. See the [Quickstart](/mcp/quickstart#chatgpt) for the step-by-step flow. **Any other MCP client:** point it at `https://mcp.particle.pro` over Streamable HTTP and prefer OAuth if the client supports it; stdio-only clients can bridge through `mcp-remote` (last tab above). The first tool call triggers an OAuth flow: your client opens the authorize URL in a browser, you sign in to Particle Pro and pick the project the agent should act as, and your client exchanges the resulting code for a token automatically. See the [Quickstart](/mcp/quickstart) for a step-by-step walk-through and a sanity-check call, or [Authentication](/mcp/authentication) for the full OAuth 2.1 spec. ## What's in the box 25 tools, organized into **exposure categories**. A bare connection advertises the six default categories plus the always-on `system` meta-tools; five more categories are **opt-in** (advertised only when you select them with the `?include=` selector). Either way, **every public tool is callable by name** regardless of what was advertised — see [Tool sets & discovery](/mcp/tool-sets). Default categories — resolve podcasts/people/companies, search transcripts, list episodes, pull mentions, browse topics, chart rankings, and guests. Opt-in categories — sponsor analytics, publisher profiles, listener ratings, and corpus-wide bias/brand-suitability views. `particle_catalog` and `particle_call` — always on, so a default agent can discover and call the rest of the surface. The selector mechanics and the contract that discovery is free while execution is metered. | | | | ------------------ | ------------------------------------------------------------------------------------------------------------------ | | **Endpoint** | `https://mcp.particle.pro` | | **Transport** | [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) | | **Authentication** | OAuth 2.1 (interactive clients) or platform API keys (headless agents) — see [Authentication](/mcp/authentication) | | **Tool count** | 25 across 12 exposure categories (6 default + always-on `system`, 5 opt-in) | | **Tool names** | Flat `particle_*` identifiers; every public tool is callable by name | | **Read/write** | Read-only (every tool advertises `readOnlyHint: true`) | ## How the tools compose The tools are not isolated lookups — they expose one connected knowledge graph, and two conventions carry most of the design: * **Lean by default, expand on demand.** Most tools return a minimal payload and opt into richer sections via an `include` array (a company's people, products, and competitors; a person's roles and podcast appearances; an episode's segments, clips, topics, and transcript) or a `mode`/`format` switch. A tool does far more than its name implies — check its input schema (or the `↳` expand hints in [`particle_catalog`](/mcp/tools/system/catalog)) before assuming a capability is missing. * **Slugs are edges.** Every slug a tool returns — person, company, podcast, episode, publisher, guest — is a valid input to the other tools. Resolve a free-text name once, then traverse: `particle_company_resolve("Andreessen Horowitz")` → `particle_company_get` with `include: ["people"]` → `particle_person_get` with `include: ["podcast_appearances"]` → `particle_podcast_get_episode` with `include: ["transcript", "entities"]`. Four hops from a company name to every entity discussed in an episode one of its partners appeared on. Connected clients get the same guidance in-band: the server's `initialize` instructions state both conventions on every connect, and `particle_catalog` surfaces each tool's expand options. ## How it differs from the REST API The REST API and the MCP server share a single backing process and the same handlers. The differences: * **Shape.** Each MCP tool returns one markdown text content block. Pass `output_format: "json"` to receive the same response as compact JSON in that block instead. If you need typed JSON with a stable schema for programmatic use, hit the underlying REST endpoint linked from each tool page. * **Bundling.** Some MCP tools roll up multiple REST calls (e.g. `particle_podcast_get_episode` can also fetch top entities, segments, clips, topics, and the transcript via its `include` array). REST endpoints are single-resource. * **Identifiers.** MCP tools always accept the agent-facing slug (`person_slug`, `company_slug`, `podcast_slug`, `episode_slug`, `publisher_slug`). The REST API accepts the same slugs alongside encoded canonical IDs. * **Auth.** Interactive MCP clients use OAuth 2.1 access tokens bound to `https://mcp.particle.pro` as their `aud` claim; headless agents may send the same `pp_*` API keys the REST API accepts. See [Authentication](/mcp/authentication). * **Errors.** MCP tools surface errors as `isError: true` results with a human-readable message rather than HTTP problem-details bodies. See [Errors](/mcp/errors). ## When to use which surface * Building an **agent** (Claude Code, Cursor, Claude Desktop, custom MCP client)? Use the MCP server. The bundled tool responses, slug-first inputs, and markdown rendering are designed for agent loops. * Building a **service** (server-to-server, scheduled job, or the backend behind your frontend — API keys must stay server-side)? Use the REST API. Fine-grained control, OpenAPI schema, predictable payloads. * Both surfaces talk to the same database, the same search ranking, and the same billing meter, so mixing them within one organization is fine. ## Discovery Agents that follow the MCP / OAuth specs discover everything they need from the resource: * `https://mcp.particle.pro/.well-known/oauth-protected-resource` — RFC 9728 protected-resource metadata pointing at the AS. * A 401 from the MCP endpoint carries `WWW-Authenticate: Bearer realm="mcp", error="...", resource_metadata="..."` so unauthenticated clients can bootstrap. The Authorization Server's metadata document at `https://api.particle.pro/.well-known/oauth-authorization-server` carries the rest (token endpoint, registration endpoint, supported scopes, JWKS URI). ## Next steps Step-by-step setup with troubleshooting for each client. OAuth 2.1 end-to-end: discovery, registration, PKCE, refresh, revocation. Every tool, every input, every output. Exposure categories, the include/exclude selectors, and the always-callable contract. How tool errors surface and how agents should react. # Quickstart Source: https://docs.particle.pro/mcp/quickstart Connect the Particle Pro MCP server to Claude Code, Claude Desktop, Cursor, VS Code, ChatGPT, Windsurf, Zed, an LLM API, or a custom MCP client. The Particle Pro MCP server runs at **`https://mcp.particle.pro`** over Streamable HTTP. Point any modern MCP client at the URL and the client handles the OAuth handshake automatically. ## Install **One-click install:** [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=particle\&config=eyJ1cmwiOiJodHRwczovL21jcC5wYXJ0aWNsZS5wcm8ifQ%3D%3D) · [Add to VS Code](https://vscode.dev/redirect/mcp/install?name=particle\&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.particle.pro%22%7D) — both encode the same config as the manual tabs below, so use whichever you prefer. ```bash Claude Code theme={"dark"} claude mcp add --transport http particle https://mcp.particle.pro ``` ```json Claude Desktop theme={"dark"} // ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) // %APPDATA%\Claude\claude_desktop_config.json (Windows) // // Claude Desktop's claude_desktop_config.json does not accept a bare // `url` for remote MCP servers — only stdio `command`/`args` entries. // Bridge the remote server through `mcp-remote`, which opens a browser // for OAuth and proxies stdio ↔ Streamable HTTP. Restart Claude Desktop // after editing. { "mcpServers": { "particle": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.particle.pro"] } } } ``` ```json Cursor theme={"dark"} // ~/.cursor/mcp.json { "mcpServers": { "particle": { "url": "https://mcp.particle.pro" } } } ``` ```json VS Code theme={"dark"} // .vscode/mcp.json { "servers": { "particle": { "type": "http", "url": "https://mcp.particle.pro" } } } ``` ```json Windsurf theme={"dark"} // ~/.codeium/windsurf/mcp_config.json — note Windsurf uses `serverUrl`, // not `url`. UI alternative: Windsurf Settings → Cascade → MCP Servers. { "mcpServers": { "particle": { "serverUrl": "https://mcp.particle.pro" } } } ``` ```json Zed theme={"dark"} // settings.json — Zed prompts for the OAuth flow on first use when no // Authorization header is configured. { "context_servers": { "particle": { "url": "https://mcp.particle.pro" } } } ``` ```bash mcp-remote (any stdio-only client) theme={"dark"} # Bridges a stdio MCP client to a remote OAuth-secured Streamable HTTP server. npx -y mcp-remote https://mcp.particle.pro ``` **Agent-assisted setup:** in any coding agent that can edit files (Claude Code, Cursor), you can skip the manual config entirely — paste: ```text theme={"dark"} Add the Particle Pro MCP server to this client's MCP configuration. It is a remote Streamable HTTP server at https://mcp.particle.pro using OAuth (no command/args, no API key in the config). Find this client's MCP config file, add it under the name "particle", and tell me how to trigger the OAuth approval. ``` **Claude Desktop alternative — UI flow:** Settings → Connectors → **Add custom connector**, paste `https://mcp.particle.pro`, complete OAuth in the browser. Easier than editing JSON and avoids the `mcp-remote` bridge entirely. ### ChatGPT ChatGPT connects through its connector UI rather than a config file (requires a Pro, Plus, Business, Enterprise, or Edu plan): 1. Open **Settings → Apps → Advanced settings** and enable **Developer mode**. 2. Back in **Settings → Apps**, choose **Create app** and enter `https://mcp.particle.pro` as the MCP server URL, with **OAuth** as the authentication method. 3. Complete the OAuth approval in the browser pop-up, picking the project the agent should act on. 4. In a conversation, enable the connector from the composer's tools menu, then run the [sanity check](#sanity-check) below. See [OpenAI's developer-mode guide](https://platform.openai.com/docs/guides/developer-mode) for connector specifics on their side. ### Calling it from an LLM API Server-side agents can hand the MCP server straight to a model API — no MCP client library needed. Both providers connect to `https://mcp.particle.pro` themselves; authenticate with a [project API key](/mcp/authentication#api-keys-for-headless-agents) as the bearer: ```bash Anthropic Messages API theme={"dark"} curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mcp-client-2025-11-20" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 1024, "messages": [{"role": "user", "content": "Which podcasts discussed Anthropic this week?"}], "mcp_servers": [{ "type": "url", "url": "https://mcp.particle.pro", "name": "particle", "authorization_token": "'$PARTICLE_API_KEY'" }], "tools": [{"type": "mcp_toolset", "mcp_server_name": "particle"}] }' ``` ```bash OpenAI Responses API theme={"dark"} curl https://api.openai.com/v1/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -d '{ "model": "gpt-5.5", "tools": [{ "type": "mcp", "server_label": "particle", "server_url": "https://mcp.particle.pro", "authorization": "'$PARTICLE_API_KEY'", "require_approval": "never" }], "input": "Which podcasts discussed Anthropic this week?" }' ``` Every tool is read-only (`require_approval: "never"` is safe here); calls meter to the project that owns the key exactly like any other MCP traffic. ### Any other client Point the client at `https://mcp.particle.pro` over Streamable HTTP and prefer OAuth when the client supports it. Stdio-only clients bridge through `mcp-remote` (tab above). Headless or programmatic callers should read [Authentication](/mcp/authentication). You'll need a Particle Pro account with an active project. If you don't have one, [sign up at platform.particle.pro](https://platform.particle.pro) first. ## OAuth flow The first tool call triggers an OAuth flow: 1. Your client opens `https://api.particle.pro/oauth/authorize?...` in a browser. 2. You sign in to Particle Pro (if not already), then approve the requested scopes (`mcp:read`, `mcp:write` by default) and pick which project the agent should act on. 3. The browser redirects back to your client's local callback. The client exchanges the authorization code for tokens, stores them, and re-issues the original tool call. Subsequent calls reuse the cached tokens. Refresh happens automatically — see [Authentication](/mcp/authentication) for the full flow and the dynamic-client-registration details. ## Sanity check Once connected, ask the agent to call a low-cost read tool. A good first call: ```text theme={"dark"} Use particle particle_entity_resolve to look up "Marc Andreessen". ``` Expected response (every tool returns a single markdown text block; pass `output_format: "json"` to receive the structured JSON form instead): ```markdown theme={"dark"} ## Entity matches for "Marc Andreessen" (1) ### Marc Andreessen - **Type:** person - **Person slug:** marc-andreessen - **Title:** General Partner - **Company:** Andreessen Horowitz - **Wikipedia:** https://en.wikipedia.org/wiki/Marc_Andreessen ``` The handle row — `- **Person slug:**` for a person — is what every `person_slug` parameter expects (`marc-andreessen` here). Feed it into `particle_podcast_find_mentions`, `particle_podcast_search_transcripts`, or `particle_podcast_list_episodes`. (Company results lead with `- **Type:** company` and a `- **Company slug:**` row plus `- **Ticker:**` / `- **Domain:**`.) ## A short agent loop A typical research workflow chains a small number of tools: 1. **Resolve names to slugs** — `particle_entity_resolve` for any named thing (returns a typed handle), `particle_person_resolve` for people only, `particle_company_resolve` for orgs by ticker/domain, `particle_podcast_resolve` for shows. 2. **Discover episodes or dialogue** — `particle_podcast_search_transcripts` for topic-based ranking (relevant clips arrive inline), `particle_podcast_find_mentions` for "every line about X", `particle_podcast_list_episodes` for filter-driven discovery. 3. **Drill in** — `particle_podcast_get_episode` with `include: ["segments", "clips", "transcript"]` for the full picture. 4. **Pivot to ads, rankings, and brand safety** — opt-in categories like `particle_company_get_podcast_ad_presence` ("where does company X advertise"), `particle_podcast_get_rankings` (chart movers/history), and `particle_podcast_get_suitability_leaderboard` (safest publishers). These are callable by name even when not advertised — see [Tool sets & discovery](/mcp/tool-sets). Every output is a single markdown text block. Agents read the bolded KV labels to extract identifiers and chain them into the next call. Two shapes appear: top-level summary lines under `## Heading` use flat `**Key:** value`, while per-item field rows under `### Item title` use `- **Key:** value` bullets. Either way, the bold-key prefix (e.g. `**Slug:**`, `**Episode slug:**`) is what to grep for. ## Troubleshooting * **"server requires authentication"** — your client hit the MCP endpoint without a bearer token, or the token failed verification. Re-run the OAuth flow; if you persist tokens to disk, delete the cache for `mcp.particle.pro`. * **Browser callback hangs** — confirm the client's `redirect_uri` matches one it registered. Most desktop MCP clients use `http://localhost:/callback`; the AS allows localhost redirects without prior approval. * **403 on a tool call after a successful connection** — the OAuth grant is for the wrong project. Revoke the connection ([Manage connections](/mcp/authentication#manage-connections)) and reconnect, picking the correct project at the consent screen. * **Specific tool errors** — see the [Errors](/mcp/errors) page for how tool failures surface as MCP `isError: true` results and how to react to each kind. ## Next steps OAuth 2.1 end-to-end — useful when wiring up a custom client. Every input, output, and example for every tool. Default vs opt-in categories, the include/exclude selectors, and the meta-tools. # Skill bundles Source: https://docs.particle.pro/mcp/skills Pre-built agent skills — a curated tool URL plus a copy-paste SKILL.md — for the three workflows people run most. A skill bundle is two things: a connection URL that advertises **only the tools a workflow needs** (via the [`?tools=` selector](/mcp/tool-sets#selecting-exact-tools)), and a **SKILL.md** that teaches the agent the workflow's conventions — which tool to start from, how slugs chain, and the gotchas that cost a wasted round-trip. Paste both and the agent is productive on the first call. Skills only shape what's *advertised*; every public tool stays callable by name, and the always-on `particle_catalog` / `particle_call` meta-tools ride along with every bundle, so an agent can always discover and reach the rest of the surface. To install a SKILL.md in Claude Code, save it as `.claude/skills//SKILL.md` in your project (or `~/.claude/skills//SKILL.md` globally). Claude Code picks up changes to existing skill directories automatically; if this is your first skill (a brand-new `skills/` directory), start a new session so it's discovered. Other harnesses: paste the body into your system prompt. ## Podcast Intelligence Find what's being said across the catalog: search dialogue, scan episodes, drill into transcripts. ```bash Connect (Claude Code) theme={"dark"} claude mcp add --transport http particle-podcasts "https://mcp.particle.pro?tools=particle_entity_resolve,particle_podcast_resolve,particle_podcast_search_transcripts,particle_podcast_list_episodes,particle_podcast_get_episode" ``` ```markdown SKILL.md theme={"dark"} --- name: particle-podcast-intelligence description: Search and analyze podcast dialogue via the Particle Pro MCP server — resolve shows and entities to slugs, search transcripts semantically, list episodes by filters, and pull full episode detail. --- # Particle Podcast Intelligence Tools: particle_entity_resolve, particle_podcast_resolve, particle_podcast_search_transcripts, particle_podcast_list_episodes, particle_podcast_get_episode. ## Workflow 1. **Resolve names to slugs first — never guess a slug.** Free-text name of any kind → `particle_entity_resolve` (returns a typed handle: person, company, place, other). Show title, iTunes ID, or RSS URL → `particle_podcast_resolve`. 2. **Search by intent.** Dialogue *about* a topic → `particle_podcast_search_transcripts` (`semantic_search` for paraphrase, `keyword_search` for exact phrases, both for hybrid ranking; at least one is required). Filter-driven discovery (by podcast, person, company, language, date, duration) → `particle_podcast_list_episodes`. 3. **Drill in.** `particle_podcast_get_episode` with `include: ["segments", "entities", "clips", "topics", "transcript"]` — request only the sections you need; the default response is lean. ## Conventions - Every slug a tool returns is a valid input to the other tools — resolve once, then traverse. - `particle_entity_resolve` returns a *typed* handle: feed a `person` result's slug to `person_slug` parameters, a `company` result's slug to `company_slug`, and a `place`/`other` result's slug to `entity_slug` (the catch-all filter on search and list_episodes). Don't cross the streams — a place slug in `person_slug` resolves nothing. - Search matches return bounded transcript windows; raise `context` to widen them in place instead of fetching the full transcript. - There is no separate clip-search tool: relevant clips arrive inline on search matches, and a known episode's full clip list is `get_episode` with `include: ["clips"]`. - `particle_podcast_search_transcripts` is a premium-tier tool; the others here are standard. ``` Try: *"What have podcast hosts said about AI agents replacing SaaS in the last 90 days?"* · *"Find the All-In episode where they discussed Anthropic and summarize the segment."* ## Company Deep Dive One company in full: profile, people, competitors, podcast footprint, and sponsorships. ```bash Connect (Claude Code) theme={"dark"} claude mcp add --transport http particle-companies "https://mcp.particle.pro?tools=particle_company_resolve,particle_company_get,particle_person_get,particle_podcast_find_mentions,particle_company_get_podcast_ad_presence" ``` ```markdown SKILL.md theme={"dark"} --- name: particle-company-deep-dive description: Research a company via the Particle Pro MCP server — resolve by name/ticker/domain, pull the bundled profile with people/products/competitors, trace podcast mentions, and map its podcast sponsorships. --- # Particle Company Deep Dive Tools: particle_company_resolve, particle_company_get, particle_person_get, particle_podcast_find_mentions, particle_company_get_podcast_ad_presence. ## Workflow 1. **Resolve.** `particle_company_resolve` takes a free-text name (`query`, one company at a time) or a `ticker`/`domain`/`cik`/`qid` — those four accept comma-separated values for bulk lookup — and returns the canonical `company_slug`. 2. **Profile.** `particle_company_get` with `include: ["people", "products", "competitors"]` — each person in the response carries a `person_slug`. 3. **Follow the people.** `particle_person_get` with `include: ["podcast_appearances", "companies"]` turns a slug from step 2 into role history and recent episodes (with episode/podcast slugs for follow-up). 4. **Trace the conversation.** `particle_podcast_find_mentions` with `company_slug` finds dialogue lines naming the company. Default `format: "summary"` previews ~10 mention lines per episode for scanning; call back with `format: "detail"` plus `episode_slug` (up to 10, comma-separated) for complete mention windows with context. 5. **Map the ad footprint.** `particle_company_get_podcast_ad_presence` shows where the company appears as a sponsor. ## Conventions - Slugs are edges: company → its people → a person's appearances → an episode's mentions, all in four calls. - Mentions and ad-presence are premium-tier tools; resolve/get are standard. - A 404 on a slug means it's stale or misspelled — re-resolve it, don't retry. ``` Try: *"Build a brief on Ramp: who runs it, who competes with it, and where its execs have appeared on podcasts."* · *"Which podcasts does Notion sponsor, and what do hosts say about it organically?"* ## Guest & People Research Who appears on podcasts: guest profiles, trends, and every line about a person. ```bash Connect (Claude Code) theme={"dark"} claude mcp add --transport http particle-people "https://mcp.particle.pro?tools=particle_person_resolve,particle_person_get,particle_topic_browse,particle_podcast_list_guests,particle_podcast_get_guest,particle_podcast_find_mentions" ``` ```markdown SKILL.md theme={"dark"} --- name: particle-guest-research description: Research podcast guests and people via the Particle Pro MCP server — resolve people to canonical slugs, pull profiles and appearance histories, browse the guest directory and trends, and trace mentions. --- # Particle Guest & People Research Tools: particle_person_resolve, particle_person_get, particle_topic_browse, particle_podcast_list_guests, particle_podcast_get_guest, particle_podcast_find_mentions. ## Workflow 1. **Resolve.** `particle_person_resolve` turns a free-text name (comma-separated for bulk) into the canonical `person_slug` every other tool accepts. 2. **Profile.** `particle_person_get` with `include: ["external_links", "podcast_appearances", "companies"]` for bio, links, recent appearances, and role history. 3. **Guest analytics.** `particle_podcast_get_guest` (same slug as `guest_slug`) for appearance stats and per-podcast rollups. `particle_podcast_list_guests` with `mode: "directory"` ranks by lifetime appearances; `mode: "trends"` surfaces who's appearing across multiple shows right now; `topic_slug` (a slug from `particle_topic_browse`) narrows either to a topic. 4. **What's said about them.** `particle_podcast_find_mentions` with `person_slug` — `format: "summary"` to scan (~10 lines per episode), `format: "detail"` + `episode_slug` for the full mention windows. ## Conventions - `person_slug` and `guest_slug` are the same identifier — resolve once, use it everywhere. - Some people lack a knowledge-graph link; mention lookups for them return an explained empty result, not an error — report that, don't retry. - `find_mentions` is premium-tier; the rest here are standard. ``` Try: *"Who are the trending AI guests this month, and which shows had them?"* · *"Everything said about Jensen Huang on podcasts this quarter, organized by theme."* ## Build your own A skill is just a `?tools=` URL plus instructions. Pick tools from the [tool reference](/mcp/tools/overview), pin them on the connection URL, and teach the loop: **resolve → traverse → expand with `include` only when needed**. The [llms.txt](https://docs.particle.pro/llms.txt) agent instructions are a ready-made starting point for the conventions section. # Tool sets & discovery Source: https://docs.particle.pro/mcp/tool-sets How Particle tools are grouped into exposure categories, how the include/exclude selectors shape your tools/list, and why every public tool stays callable by name. Every Particle MCP tool belongs to exactly one **exposure category**. Categories are the unit of `tools/list` advertisement: some advertise on a bare connection (default), others only when you ask for them (opt-in). But the advertisement is just a menu — **every public tool is always callable by name**, no matter what was advertised. Discovery is free; execution is metered and plan-gated. ## Categories | Category | Exposure | Summary | | --------------------- | --------- | ----------------------------------------------------------------------------------------- | | `system` | always on | Discovery meta-tools: browse the full tool catalog and call any tool by name. | | `podcasts` | default | Resolve podcasts, list and fetch episodes, search transcripts, and find entity mentions. | | `people` | default | Resolve people and entities to canonical handles and fetch person profiles. | | `companies` | default | Resolve companies and fetch company profiles with people, products, and competitors. | | `topics` | default | Browse the hierarchical topic taxonomy used to classify podcast episodes. | | `podcast_rankings` | default | Podcast chart rankings: current charts, movers, and ranking history. | | `podcast_guests` | default | Podcast guest directory, trending guests, and per-guest appearance profiles. | | `podcast_advertising` | opt-in | Podcast advertising intelligence: sponsor rosters, ad presence, and sponsor leaderboards. | | `podcast_publishers` | opt-in | Podcast publisher profiles with their shows, bias profile, and suitability profile. | | `podcast_ratings` | opt-in | Listener review ratings for podcasts: summaries and recent rating lists. | | `podcast_bias` | opt-in | Corpus-wide political-bias views: publisher leaderboards and publishers by bias result. | | `podcast_suitability` | opt-in | Corpus-wide GARM brand-suitability views: publisher leaderboards and category exposure. | A bare connection advertises **`system` plus every default category**. The five opt-in categories advertise only when selected. ## Selecting categories The selector lives on the connection — it shapes what `tools/list` advertises, nothing else. ### Query parameters Append `?include=` (and/or `?exclude=`) to the connection URL. Values are **comma-separated** category names, case-insensitive. ```bash theme={"dark"} # Advertise the opt-in advertising + bias categories on top of the defaults. https://mcp.particle.pro?include=podcast_advertising,podcast_bias # Advertise everything. https://mcp.particle.pro?include=all # Hide a default category you don't want cluttering the list. https://mcp.particle.pro?exclude=podcast_rankings # Combine: add an opt-in category, drop a default one. https://mcp.particle.pro?include=podcast_ratings&exclude=topics ``` * `include=all` selects every declared category. * The active set is computed as **defaults ∪ include − exclude**. * **Unknown tokens are ignored** — a typo in a selector never errors `tools/list`; you just see the default surface for that token. * **`system` is irremovable** — `exclude=system` is silently ignored, so the discovery meta-tools never disappear. ### Headers Clients that can't append query strings can send the same values as headers: ``` X-Particle-Include: podcast_advertising,podcast_bias X-Particle-Exclude: podcast_rankings ``` The query parameter wins when both are present (**query > header precedence**), resolved independently for each direction (`include` and `exclude`). ## Selecting exact tools When a category is still too broad — a curated workflow that needs exactly four tools, say — the `?tools=` selector pins `tools/list` to a comma-separated list of tool names: ```bash theme={"dark"} # Advertise exactly these tools (plus the always-on system meta-tools). https://mcp.particle.pro?tools=particle_podcast_resolve,particle_podcast_search_transcripts,particle_podcast_get_episode ``` * A non-empty `?tools=` selection **overrides** `include`/`exclude` entirely. * Named tools advertise **regardless of category exposure** — an opt-in tool named here shows up without its category. * The `system` meta-tools are always advertised alongside the selection, so discovery never disappears. * Unknown names are ignored; if *every* name is unknown, the selection is empty and the connection falls back to the category selectors above (defaults ∪ `include` − `exclude`). * Same header fallback (`X-Particle-Tools`) and query-over-header precedence as the category selectors. * And the same contract: this shapes **advertisement only** — every public tool stays callable by name. This is the mechanism behind the pre-built [skill bundles](/mcp/skills): each skill embeds a `?tools=` URL so the agent sees only the tools its workflow needs. ## Every public tool is callable by name The selector only governs the **menu** (`tools/list`). `tools/call` is selector-independent: every public tool stays executable by name whether or not it was advertised on your connection. That separation is deliberate: * **Discovery is free.** A bare connection can learn the entire surface via [`particle_catalog`](/mcp/tools/system/catalog) and call any of it — no reconnection, no selector change. * **Execution is metered and plan-gated.** Calling a tool runs the same billing meter, pricing gate, and premium-tier gate regardless of how you reached it. Advertisement changes visibility, never entitlement. So the opt-in categories exist to keep a default agent's tool list focused, not to lock anything away. If your harness lets you call tools it didn't see advertised, just call them. If it doesn't, use the meta-tools below. ## Meta-tools Two always-on `system` tools make the surface self-expanding. Browse the full catalog. No arguments → the category menu; with a `category` → full input schemas for that category's tools. Dispatch any public tool by name — the fallback for harnesses that block un-advertised tool names. ### `particle_catalog` ```text theme={"dark"} # See every category and its tools. particle_catalog {} # Get the full input schema for an opt-in category before calling it. particle_catalog { "category": "podcast_bias" } ``` ### `particle_call` ```text theme={"dark"} # Reach an opt-in tool when the harness blocks un-advertised tool_use names. particle_call { "tool": "particle_podcast_get_bias_leaderboard", "arguments": { "metric": "most_right_leaning", "limit": 10 } } ``` Identical metering and plan gating apply whether you call a tool directly or through `particle_call`. ## Next steps Every tool, grouped by category, with inputs and outputs. Connect a client and run a first call. # particle_company_get Source: https://docs.particle.pro/mcp/tools/companies/company-get Bundled profile for one company — identifiers, optional people, product hierarchy, and competitor list. Return a bundled profile for one company: identifiers (slug, ticker, domain, CIK, QID, linked entity), name, and description. The default response is lean. Request the heavier sub-resources with `include`: `people` for current leadership and notable people, `products` for the three-level product hierarchy, `competitors` for the competitor list. For sponsor/advertising analytics on this company, use [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence) instead. ## Inputs | Field | Type | Required | Default | Description | | ---------------- | -------------- | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `company_slug` | string | yes | — | Company identifier — accepts slug (e.g. `"nvidia"`), domain (e.g. `"nvidia.com"`), or canonical ID. If you already know the domain you can call this tool directly without first running `particle_company_resolve`. | | `include` | array of enums | no | `[]` | Optional response sections: `people` (current leadership and notable people; person slugs feed `particle_person_get`), `products` (three-level segment → product line → product hierarchy), `competitors` (competitor list, paginated to 25). Request only what you need. | | `product_status` | string | no | `"active"` | Comma-separated lifecycle filter for `include: ["products"]`. Allowed values: `active`, `announced`, `discontinued`, `rumored`. | ## Output A markdown document with the company name as an H2, identifier KV lines (`**Slug:**`, `**Ticker:**`, `**Domain:**`, `**CIK:**`, `**QID:**`, `**Entity slug:**` — only those that have values, and deduplicated: `**Slug:**` is omitted when it would repeat the Domain or ID rows, and `**Entity slug:**` when it would repeat `**Slug:**`. As the chaining handle, use `**Slug:**` when present, otherwise `**Domain:**` — both are accepted by `company_slug` parameters; a ticker is not, so when neither row is present re-resolve via `particle_company_resolve` with the ticker), then a paragraph with the description. The following H3 sections appear when the matching `include` value was requested and data is present: * `### People` — one bullet per person, formatted `Name (slug) — title`. The parenthesized slug feeds [`particle_person_get`](/mcp/tools/people/person-get). * `### Products` — an indented bullet hierarchy `- **Name** (level, status)` (segment → product\_line → product). * `### Competitors` — one bullet per competitor: `- Name (handles) — competitive_basis`. Sample (`company_slug="nvidia", include=["people","products","competitors"]`, truncated): ```markdown theme={"dark"} ## Nvidia **Slug:** nvidia **Ticker:** NVDA **Domain:** nvidia.com **CIK:** 0001045810 **QID:** Q182477 Nvidia is a leading developer of graphics processing units. … ### People - Jensen Huang (jensen-huang) — Founder & CEO - Colette Kress (colette-kress) — EVP & CFO ### Products - **Automotive** (segment, active) - **NVIDIA DRIVE** (product_line, active) - **DRIVE Hyperion Platform** (product, active) - **Compute & Networking** (segment, active) - **Data Center Accelerators** (product_line, active) - **Blackwell GPUs** (product, active) ### Competitors - Raspberry Pi Holdings plc (uetersen, raspberrypi.com) — NVIDIA competes with Raspberry Pi in edge AI and robotics segments through its Jetson series… ``` ## Example ```text theme={"dark"} Agent calls: particle_company_get { "company_slug": "nvidia", "include": ["people", "products", "competitors"], "product_status": "active,announced" } ``` ## Related * REST equivalent: [`GET /v1/companies/{id}`](/companies/overview), with sub-resources for people, products, and competitors. * For sponsor analytics, use [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence). * For dialogue mentioning the company, use [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) with `company_slug`. # particle_company_resolve Source: https://docs.particle.pro/mcp/tools/companies/company-resolve Resolve a company by free-text name, ticker, SEC CIK, Wikidata QID, or domain. Resolve a company by free-text name, ticker, SEC CIK, Wikidata QID, or domain. Returns candidates with the agent-facing identifier (`slug`, falling back to `domain` or `id`) you can pass to [`particle_company_get`](/mcp/tools/companies/company-get), [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence), [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) (as `company_slug`), or [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes). At least one identifier is required. Multiple are ANDed together — useful for disambiguating (e.g. ticker plus a name hint). For people or other knowledge-graph entities (not companies) use [`particle_entity_resolve`](/mcp/tools/people/entity-resolve) instead. ## Inputs At least one of `query`, `ticker`, `domain`, `cik`, or `qid` is required. | Field | Type | Required | Default | Description | | -------- | -------------- | -------- | ------- | --------------------------------------------------------------------------------------- | | `query` | string | one of | — | Free-text company name (case-insensitive). Use for human-typed names. | | `ticker` | string | one of | — | Stock ticker symbol (e.g. `"NVDA"`, `"AAPL"`). Comma-separated for multi-ticker lookup. | | `domain` | string | one of | — | Company website domain (e.g. `"apple.com"`). Comma-separated for bulk. | | `cik` | string | one of | — | SEC Central Index Key (e.g. `"0000320193"`). Comma-separated for bulk. | | `qid` | string | one of | — | Wikidata QID (e.g. `"Q312"`). Comma-separated for bulk. | | `limit` | integer (1–25) | no | 5 | Maximum candidates to return. | ## Output A markdown document with `## Company matches (N)` and one `### Name` section per result. Each section carries bulleted KV rows: `- **Slug:**`, `- **ID:**`, `- **Description:**`, `- **Ticker:**`, `- **Domain:**`, `- **CIK:**`, `- **QID:**` — only those that have values appear. The `- **Slug:**` row is omitted when the slug equals the domain or the ID (to avoid duplication). Sample (`ticker="NVDA"`): ```markdown theme={"dark"} ## Company matches (1) ### Nvidia - **Slug:** nvidia - **ID:** 3CensCwu5G2oKCFgPrNf89 - **Description:** Nvidia is a leading developer of graphics processing units. … - **Ticker:** NVDA - **Domain:** nvidia.com - **CIK:** 0001045810 - **QID:** Q182477 ``` The string after `- **Slug:**` (or `- **Domain:**` if no slug row is present, or `- **ID:**` as a final fallback) is what you pass to other tools as `company_slug` — every tool that takes `company_slug` accepts knowledge-graph slug, domain, or encoded ID interchangeably. ## Example ```text theme={"dark"} Agent calls: particle_company_resolve { "ticker": "NVDA" } → reads "- **Slug:**" row → "nvidia" Agent calls: particle_company_get_podcast_ad_presence { "company_slug": "nvidia" } ``` ## Related * REST equivalent: [`GET /v1/companies`](/companies/overview). * For a richer single-company view (people, products, competitors), use [`particle_company_get`](/mcp/tools/companies/company-get). * For sponsor analytics, use [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence). # Tool reference Source: https://docs.particle.pro/mcp/tools/overview Every tool the Particle Pro MCP server exposes, grouped by exposure category. The Particle Pro MCP server exposes **25 tools across 12 exposure categories**. Each tool wraps an existing REST handler — the per-tool pages link to the analogous REST endpoint when one exists. Tools belong to **default** categories (advertised on a bare connection) or **opt-in** categories (advertised only when selected via the `?include=` selector). Either way, every public tool is **callable by name** regardless of what was advertised — see [Tool sets & discovery](/mcp/tool-sets) for the selector mechanics and the always-callable contract. Every tool advertises `readOnlyHint: true`. Inputs use snake\_case JSON; outputs are a single markdown text block (`content[].text`), and passing `output_format: "json"` swaps that block for the compact JSON form of the same response. Tool names are flat `particle_*` identifiers. Read this list through two conventions (the server states both in its `initialize` instructions): tools are **lean by default** — `include` arrays and `mode`/`format` switches expand them well beyond their one-line summaries — and **slugs are edges**: any slug one tool returns is a valid input to the others, so resolve a name once, then traverse the graph (company → its people → a person's appearances → that episode's transcript and entities). See [How the tools compose](/mcp/overview#how-the-tools-compose). ## System Always-on discovery meta-tools — the surface is self-expanding from a bare connection. Browse the full tool catalog; with a category, full input schemas. Dispatch any public tool by name (fallback for un-advertised tools). ## Podcasts default Resolve podcasts, list and fetch episodes, search transcripts, and find entity mentions. Find a podcast by title, slug, iTunes ID, or RSS URL; optional hydrations. Bundled episode overview — speakers, entities, segments, clips, topics, transcript. Every dialogue line where a person or company is named. Semantic, keyword, or hybrid search over dialogue, segments, and clips. Filter-driven episode discovery across the catalog. ## People default Resolve people and entities to canonical handles and fetch person profiles. Union resolver — person, company, place, or other, with a typed handle. People-only resolver — canonical person slug. Person profile — role, bio, external links, appearances, role history. ## Companies default Resolve companies and fetch company profiles with people, products, and competitors. Resolve a company by name, ticker, domain, CIK, or QID. Bundled company profile — identifiers, people, products, competitors. ## Topics default Browse the hierarchical topic taxonomy used to classify podcast episodes. Navigate the topic taxonomy — roots and direct children. ## Podcast rankings default Podcast chart rankings: current charts, movers, and ranking history. Apple/Spotify charts in three modes — chart, movers, history. ## Podcast guests default Podcast guest directory, trending guests, and per-guest appearance profiles. Guest directory (lifetime) or trending guests (last 30 days). A guest's appearance profile, per-podcast rollup, suitability exposure. ## Podcast advertising opt-in Sponsor analytics — per-company, per-podcast, and across the catalog. Add `?include=podcast_advertising` to advertise these on `tools/list`. Where company X shows up as a sponsor. Who sponsors podcast Y, plus aggregate stats. Top advertisers across the catalog. ## Podcast publishers opt-in Publisher profiles with their shows, bias profile, and suitability profile. Add `?include=podcast_publishers`. A publisher's profile — shows, bias profile, suitability profile. ## Podcast ratings opt-in Listener review ratings for podcasts. Add `?include=podcast_ratings`. Aggregate summary, per-platform breakdown, sentiment, recent reviews. ## Podcast bias opt-in Corpus-wide political-bias views. Add `?include=podcast_bias`. Rank publishers by political-bias metrics. Which publishers have the most podcasts in one bias bucket. ## Podcast suitability opt-in Corpus-wide GARM brand-suitability views. Add `?include=podcast_suitability`. Rank publishers by IAB/GARM brand-suitability composition. Publishers most (or least) exposed to one GARM category. ## Tiers Every tool carries a billing tier, surfaced in [`particle_catalog`](/mcp/tools/system/catalog): | Tier | Tools | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Free** | The `system` meta-tools — `particle_catalog`, `particle_call` (the dispatched inner tool meters at its own tier). Discovery never costs anything. | | **Standard** | Everything not listed below — all resolvers, `particle_person_get`, `particle_company_get`, `particle_podcast_get_episode`, `particle_podcast_list_episodes`, `particle_topic_browse`, guests, publisher, and ratings tools. | | **Premium** | `particle_podcast_search_transcripts`, `particle_podcast_find_mentions`, `particle_podcast_get_rankings`, the three `podcast_advertising` tools, both `podcast_bias` tools, and both `podcast_suitability` tools. | Premium tools require a plan that includes premium endpoints; on plans without them the call returns an `isError` result with code [`premium_required`](/errors/premium_required) and upgrade guidance. See [Errors](/mcp/errors) for how billing failures surface. ## Safety Every tool is read-only (`readOnlyHint: true`) — no tool mutates your Particle Pro data, spends money beyond metered usage, or acts on external systems. Still, tool *outputs* contain third-party content (transcripts, reviews, show notes), so when you combine this server with other MCP servers that **can** take actions, keep your client's human confirmation enabled for those action tools to avoid prompt-injection chains from untrusted content. Missing a tool you need? Email [api@particle.pro](mailto:api@particle.pro) — the surface grows in the direction people pull it. ## Choosing the right tool | I want… | Use this | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | To turn a name into a slug (any kind) | [`particle_entity_resolve`](/mcp/tools/people/entity-resolve) | | To turn a person's name into a slug | [`particle_person_resolve`](/mcp/tools/people/person-resolve) | | To turn a ticker, domain, or CIK into a company | [`particle_company_resolve`](/mcp/tools/companies/company-resolve) | | Every line where a person or company is named | [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) | | Dialogue *about* a topic (paraphrase or exact), with overlapping clips | [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts) | | Episode-level discovery with rich filters | [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes) | | A full episode overview (segments, clips, topics, transcript) | [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) | | A podcast's chart movers or history | [`particle_podcast_get_rankings`](/mcp/tools/podcast_rankings/podcast-get-rankings) | | Who's a frequent or trending podcast guest | [`particle_podcast_list_guests`](/mcp/tools/podcast_guests/podcast-list-guests) | | "Where does company X advertise" | [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence) | | "Who sponsors podcast Y" | [`particle_podcast_get_sponsors`](/mcp/tools/podcast_advertising/podcast-get-sponsors) | | Catalog-wide top advertisers | [`particle_podcast_get_sponsor_leaderboard`](/mcp/tools/podcast_advertising/podcast-get-sponsor-leaderboard) | | A publisher's bias / suitability profile | [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher) | | Which publishers lean hardest left/right | [`particle_podcast_get_bias_leaderboard`](/mcp/tools/podcast_bias/podcast-get-bias-leaderboard) | | The safest / riskiest publishers to advertise on | [`particle_podcast_get_suitability_leaderboard`](/mcp/tools/podcast_suitability/podcast-get-suitability-leaderboard) | | To discover or call a tool you haven't seen advertised | [`particle_catalog`](/mcp/tools/system/catalog) / [`particle_call`](/mcp/tools/system/call) | # particle_entity_resolve Source: https://docs.particle.pro/mcp/tools/people/entity-resolve Resolve any named thing — person, company, place, or other entity — by free-text name in one union search. Resolve any named thing — person, company, place, or other entity — by free-text name in **one union search**. Each candidate carries a `type` (`person`, `company`, `place`, or `other`) and the canonical `slug` for that type. Use this first whenever you only have a name and don't know what kind of thing it names. The slug each result hands back is already the right handle for that type's downstream tools — no second resolve step: * **`person`** → the canonical person slug. Feed it into [`particle_person_get`](/mcp/tools/people/person-get), every `person_slug` parameter ([`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions), [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts), [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes)), or [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest)'s `guest_slug`. * **`company`** → the canonical company slug. Feed it into [`particle_company_get`](/mcp/tools/companies/company-get) and every `company_slug` parameter. * **`place`** / **`other`** → a bare entity slug. It still works as a filter in `person_slug` parameters for non-person entities. If you already know it's a person, [`particle_person_resolve`](/mcp/tools/people/person-resolve) ranks people only; for companies with a known ticker, domain, CIK, or QID, [`particle_company_resolve`](/mcp/tools/companies/company-resolve) has more identifier surface. For bulk resolution, pass a comma-separated `query` — each name is resolved independently in a single call and `limit` applies per query. ## Inputs | Field | Type | Required | Default | Description | | ------- | -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | yes | — | Free-text name(s) of a person, organization, place, or company (e.g. `"sam altman"`, `"nvidia"`, `"davos"`). Case-insensitive. Comma-separated for bulk lookup — each query is resolved independently and grouped in the response. | | `limit` | integer (1–10) | no | 5 | Maximum candidates per query. | ## Output A markdown document with one section per input query, separated by horizontal rules in multi-query responses. * **Queries with matches** render as a `## Entity matches for "" (N)` heading followed by one `### Name` section per result. Each result leads with `- **Type:**` (`person` / `company` / `place` / `other`), then a type-aware handle row whose label matches the type — `- **Person slug:**`, `- **Company slug:**`, or `- **Entity slug:**`. Person results add `- **Title:**` and `- **Company:**` (the person's current role) when known; company results add `- **Ticker:**` and `- **Domain:**`; `- **Description:**` and `- **Wikipedia:**` appear when present. * **Queries with no matches** render as a single `No entities matched "". ...` paragraph (no H2 heading), so the caller can see which input failed without losing the rest. For person-backed results the handle row is the **canonical person slug**, not the underlying knowledge-graph entity slug. The KGE handle is suppressed deliberately — the agent must never receive a competing identifier that diverges from the canonical one. Company-backed results expose the entity slug, which *is* the canonical company slug. Sample (`query="Marc Andreessen, nvidia", limit=1`): ```markdown theme={"dark"} ## Entity matches for "Marc Andreessen" (1) ### Marc Andreessen - **Type:** person - **Person slug:** marc-andreessen - **Title:** General Partner - **Company:** Andreessen Horowitz - **Wikipedia:** https://en.wikipedia.org/wiki/Marc_Andreessen --- ## Entity matches for "nvidia" (1) ### Nvidia - **Type:** company - **Company slug:** nvidia - **Description:** Nvidia is a leading developer of graphics processing units. … - **Ticker:** NVDA - **Domain:** nvidia.com ``` The first result in each block is usually the one you want for unambiguous queries. For ambiguous names read the `- **Title:**` / `- **Description:**` / `- **Wikipedia:**` rows to disambiguate. ## Example ```text theme={"dark"} User: Find every recent podcast where Marc Andreessen is mentioned. Agent calls: particle_entity_resolve { "query": "Marc Andreessen" } → reads "- **Type:** person" + "- **Person slug:**" → "marc-andreessen" Agent calls: particle_podcast_find_mentions { "person_slug": "marc-andreessen", "limit": 10 } ``` ### Bulk resolution ```text theme={"dark"} User: Pull mentions for Sam Altman, Kara Swisher, and Marc Andreessen. Agent calls: particle_entity_resolve { "query": "Sam Altman, Kara Swisher, Marc Andreessen" } → reads each block's handle row → ["sam-altman", "kara-swisher", "marc-andreessen"] Agent calls: particle_podcast_find_mentions { "person_slug": "sam-altman", ... } (etc., one call per resolved slug) ``` ## Related * REST equivalent: [`GET /v1/entities`](/knowledge-graph/entities). * For people only, use [`particle_person_resolve`](/mcp/tools/people/person-resolve); for company-specific identifiers, use [`particle_company_resolve`](/mcp/tools/companies/company-resolve). * Use the returned slug with [`particle_person_get`](/mcp/tools/people/person-get), [`particle_company_get`](/mcp/tools/companies/company-get), [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions), [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts), or [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes). # particle_person_get Source: https://docs.particle.pro/mcp/tools/people/person-get A person's profile — name, current role, bio, and optional external links, podcast appearances, and full role history. Return a person's profile: name, current role, and bio, keyed by the canonical person slug from [`particle_person_resolve`](/mcp/tools/people/person-resolve). The default response is lean. Request optional sections via `include`: `external_links` for LinkedIn/Wikipedia/social profiles, `podcast_appearances` for their most recent podcast appearances (episode and podcast slugs included for follow-up calls), `companies` for the full role history (the current role is included and marked `(current)`). For podcast-guest analytics (appearance stats, suitability exposure, co-appearance graph) use [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest) with the same slug. ## Inputs | Field | Type | Required | Default | Description | | ------------- | -------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `person_slug` | string | yes | — | Canonical person slug from `particle_person_resolve` (e.g. `"sam-altman"`), or the encoded person ID. | | `include` | array of enums | no | `[]` | Optional response sections: `external_links` (LinkedIn, Wikipedia, social profiles), `podcast_appearances` (recent appearances with episode and podcast slugs), `companies` (the full role history; the current role is included and marked `(current)`). Request only what you need. | ## Output A markdown document with the person's name as an H2, then a `**Slug:**` row (or `**ID:**` when no slug is backfilled), a `**Current role:**` row (`Title, Company`), a `**Entity slug:**` row when it differs from the slug, and a bio paragraph. The following H3 sections appear when the matching `include` value was requested and data is present: * `### Companies` — one bullet per role, formatted `Company — Title (current)` (the `(current)` marker appears on the active role). * `### External links` — one bullet per platform, formatted `Platform: ` (or `Platform: ` for handle-only platforms). * `### Recent podcast appearances` — one bullet per appearance, formatted `Episode title (episode-slug) on Podcast title (podcast-slug) — YYYY-MM-DD`. The slugs round-trip into the podcast tools. (Capped at the 10 most recent; for the full paginated history use `particle_podcast_get_guest` with `include: ["appearances"]`.) Sample (`person_slug="sam-altman", include=["companies","podcast_appearances"]`, truncated): ```markdown theme={"dark"} ## Sam Altman **Slug:** sam-altman **Current role:** CEO, OpenAI Co-founder and CEO of OpenAI. … ### Companies - OpenAI — CEO (current) - Y Combinator — President ### Recent podcast appearances - The future of AGI (the-future-of-agi) on All-In (all-in) — 2026-04-30 - Building OpenAI (building-openai) on Lex Fridman Podcast (lex-fridman-podcast) — 2026-03-12 ``` ## Example ```text theme={"dark"} Agent calls: particle_person_get { "person_slug": "sam-altman", "include": ["external_links", "companies"] } ``` ## Related * REST equivalent: `GET /v1/people/{id}` (request/response contract in the [API reference](/api-reference/introduction)). * For appearance analytics (stats, suitability exposure), use [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest) with the same slug. * Resolve a name to a slug first with [`particle_person_resolve`](/mcp/tools/people/person-resolve) or [`particle_entity_resolve`](/mcp/tools/people/entity-resolve). # particle_person_resolve Source: https://docs.particle.pro/mcp/tools/people/person-resolve Resolve a person by free-text name to a canonical person slug. Resolve a person by free-text name. Returns ranked candidates with the canonical person `slug` — the stable handle accepted by [`particle_person_get`](/mcp/tools/people/person-get) by every `person_slug` parameter ([`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions), [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts), [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes)), and by [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest)'s `guest_slug`. This is the people-only resolver. Name discovery rides the knowledge-graph entity index: people without a knowledge-graph link yet are not name-discoverable here (their profiles still resolve by slug via `particle_person_get`). For organizations, places, or mixed/unknown entity kinds use [`particle_entity_resolve`](/mcp/tools/people/entity-resolve); for companies with a known ticker or domain use [`particle_company_resolve`](/mcp/tools/companies/company-resolve). For bulk resolution, pass a comma-separated `query` — each name resolves independently in one call and `limit` applies per query. ## Inputs | Field | Type | Required | Default | Description | | ------- | -------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | yes | — | Free-text person name (e.g. `"sam altman"`). Case-insensitive. Comma-separated for bulk lookup — each name is resolved independently and grouped in the response. | | `limit` | integer (1–10) | no | 5 | Maximum candidates per query. | ## Output A markdown document with one section per input query, separated by horizontal rules in multi-query responses. * **Queries with matches** render as `## Person matches for "" (N)` followed by one `### Name` section per result, each carrying bulleted KV rows: `- **Slug:**` (the canonical person slug), `- **Title:**` and `- **Company:**` (current role, when known), `- **Description:**`, and a `- **Note:**` row only for person-typed entities that don't yet have a canonical person record — in that case the slug carries the entity slug, which still works in `person_slug` parameters. * **Queries with no matches** render as a single `No people matched "". ...` paragraph. Sample (`query="Sam Altman", limit=1`): ```markdown theme={"dark"} ## Person matches for "Sam Altman" (1) ### Sam Altman - **Slug:** sam-altman - **Title:** CEO - **Company:** OpenAI - **Description:** Co-founder and CEO of OpenAI ``` The string after `- **Slug:**` is the canonical person handle. Feed it into any `person_slug` parameter or into [`particle_person_get`](/mcp/tools/people/person-get). Exception: when a `- **Note:**` row is present, the slug is an entity slug (no canonical person record yet) — it still works in `person_slug` *filters*, but neither [`particle_person_get`](/mcp/tools/people/person-get) nor [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest) has a person to return for it (both resolve people only). ## Example ```text theme={"dark"} User: What's Sam Altman's background? Agent calls: particle_person_resolve { "query": "Sam Altman" } → reads "- **Slug:**" row → "sam-altman" Agent calls: particle_person_get { "person_slug": "sam-altman", "include": ["companies"] } ``` ## Related * REST equivalent: [`GET /v1/entities`](/knowledge-graph/entities) (filtered to people). * For mixed/unknown entity kinds, use [`particle_entity_resolve`](/mcp/tools/people/entity-resolve). * Use the returned slug with [`particle_person_get`](/mcp/tools/people/person-get) or [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest). # particle_company_get_podcast_ad_presence Source: https://docs.particle.pro/mcp/tools/podcast_advertising/company-get-podcast-ad-presence Sponsor-side ad analytics for one company — totals, podcast/episode reach, host-read vs pre-recorded, and recent placements. In the **`podcast_advertising`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_advertising`. See [Tool sets & discovery](/mcp/tool-sets). Sponsor-side ad analytics for one company: total ads, distinct podcasts and episodes reached, host-read vs pre-recorded breakdown, and the most recent ad placements with podcast attribution. This is the "where does company X show up as a sponsor" view. For "who sponsors podcast Y" use [`particle_podcast_get_sponsors`](/mcp/tools/podcast_advertising/podcast-get-sponsors). For top advertisers across the catalog use [`particle_podcast_get_sponsor_leaderboard`](/mcp/tools/podcast_advertising/podcast-get-sponsor-leaderboard). ## Inputs | Field | Type | Required | Default | Description | | -------------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `company_slug` | string | yes | — | Company identifier — accepts slug (e.g. `"nvidia"`), domain (e.g. `"nvidia.com"`), or canonical ID. If you already know the domain you can call this tool directly without first running `particle_company_resolve`. | ## Output A markdown document with `## Ad presence for ` and KV lines for `**Total ads:**`, `**Podcast reach:**` (distinct podcasts), `**Episode reach:**` (distinct episodes), `**Read mix:**` (host-read vs pre-recorded counts). When recent placements exist, a `### Recent placements` section follows with bullets formatted `Sponsor — Product on Podcast (podcast_slug) [HOST_READ|PRE_RECORDED]`. Sample (`company_slug="shopify"`): ```markdown theme={"dark"} ## Ad presence for shopify.com **Total ads:** 11269 **Podcast reach:** 959 **Episode reach:** 9860 **Read mix:** 8205 host-read, 3064 pre-recorded ### Recent placements - Shopify — Commerce platform for building online stores on The Ben Shapiro Show (the-ben-shapiro-show) [HOST_READ] - Shopify — e-commerce platform on The Ben Shapiro Show (the-ben-shapiro-show) [HOST_READ] ``` ## Example ```text theme={"dark"} Agent calls: particle_company_get_podcast_ad_presence { "company_slug": "nvidia" } ``` ## Related * REST equivalent: [`GET /v1/companies/{id}/podcast/advertising`](/podcasts/advertising). * Inverse view: [`particle_podcast_get_sponsors`](/mcp/tools/podcast_advertising/podcast-get-sponsors). * Cross-catalog ranking: [`particle_podcast_get_sponsor_leaderboard`](/mcp/tools/podcast_advertising/podcast-get-sponsor-leaderboard). # particle_podcast_get_sponsor_leaderboard Source: https://docs.particle.pro/mcp/tools/podcast_advertising/podcast-get-sponsor-leaderboard Ranked list of the most-active sponsors across the podcast catalog, by ad count, podcast reach, or episode reach. In the **`podcast_advertising`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_advertising`. See [Tool sets & discovery](/mcp/tool-sets). Ranked list of the most-active sponsors across the podcast catalog, by ad count (default), podcast reach, or episode reach. Optionally constrain to a time window or to one company's brand family. For per-company analytics use [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence); for per-podcast use [`particle_podcast_get_sponsors`](/mcp/tools/podcast_advertising/podcast-get-sponsors). ## Inputs | Field | Type | Required | Default | Description | | -------------- | -------------- | -------- | ---------- | ----------------------------------------------------------------------- | | `metric` | string | no | `ad_count` | Ranking metric. One of: `ad_count`, `podcast_reach`, `episode_reach`. | | `since` | string | no | — | Only count ads created on or after this ISO 8601 date. | | `until` | string | no | — | Only count ads created on or before this ISO 8601 date. | | `company_slug` | string | no | — | Constrain to one sponsor's brand family by company slug, domain, or ID. | | `limit` | integer (1–50) | no | 10 | Sponsors per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Sponsor leaderboard by (N)` and a flat bullet list — one bullet per ranked sponsor, formatted `# () — N ads, N podcasts, N episodes`. The parenthesized `company_id` appears only when the sponsor resolved to a known company. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`limit=3`, default `metric=ad_count`): ```markdown theme={"dark"} ## Sponsor leaderboard by ad_count (3) - #1 Grainger (grainger.com) — 14880 ads, 655 podcasts, 10219 episodes - #2 Mint Mobile (mintmobile.com) — 11637 ads, 912 podcasts, 10322 episodes - #3 Shopify (shopify.com) — 11256 ads, 959 podcasts, 9854 episodes --- **Cursor:** r.4gfFC7 ``` The value in parentheses is the `company_id` for that sponsor — feed it into [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence) as `company_slug` (which accepts knowledge-graph slug, domain, or canonical ID interchangeably) for a deeper view of any sponsor on the leaderboard. ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_sponsor_leaderboard { "metric": "podcast_reach", "since": "2025-01-01", "limit": 25 } ``` ## Related * REST equivalent: [`GET /v1/advertising/leaderboard`](/podcasts/advertising). * Per-company analytics: [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence). * Per-podcast analytics: [`particle_podcast_get_sponsors`](/mcp/tools/podcast_advertising/podcast-get-sponsors). # particle_podcast_get_sponsors Source: https://docs.particle.pro/mcp/tools/podcast_advertising/podcast-get-sponsors Top sponsors for a single podcast plus aggregate ad stats — totals, unique sponsors, episodes with ads, host-read vs pre-recorded. In the **`podcast_advertising`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_advertising`. See [Tool sets & discovery](/mcp/tool-sets). Top sponsors for a single podcast plus aggregate ad stats: total ads, unique sponsors, episodes with ads, average ads per episode, and host-read vs pre-recorded breakdown. This is the "who sponsors podcast Y" view. For "where does company X sponsor" use [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence). For top advertisers across the catalog use [`particle_podcast_get_sponsor_leaderboard`](/mcp/tools/podcast_advertising/podcast-get-sponsor-leaderboard). ## Inputs | Field | Type | Required | Default | Description | | -------------- | ------ | -------- | ------- | ------------------------------------------------ | | `podcast_slug` | string | yes | — | Podcast slug, internal ID, or numeric iTunes ID. | ## Output A markdown document with `## Sponsors of ` and KV lines for `**Total ads:**`, `**Unique sponsors:**`, `**Episodes with ads:**`, `**Avg ads/episode:**` (two decimals), `**Read mix:**` (host-read vs pre-recorded counts). A `### Top sponsors` section follows with bullets formatted `Sponsor (company_id) — N ads across M episodes` (the parenthesized company\_id appears only when the sponsor resolved to a known company). Sample (`podcast_slug="all-in"`): ```markdown theme={"dark"} ## Sponsors of 4t9PU1WkzOroEXfBl6ia7r **Total ads:** 55 **Unique sponsors:** 30 **Episodes with ads:** 23 **Avg ads/episode:** 2.39 **Read mix:** 55 host-read, 0 pre-recorded ### Top sponsors - OKX (okx.com) — 7 ads across 7 episodes - Circle (circle.com) — 5 ads across 5 episodes - Solana (solanalabs.com) — 4 ads across 4 episodes ``` ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_sponsors { "podcast_slug": "all-in" } ``` ## Related * REST equivalent: [`GET /v1/podcasts/{id}/advertising`](/podcasts/advertising). * Inverse view: [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence). * Cross-catalog ranking: [`particle_podcast_get_sponsor_leaderboard`](/mcp/tools/podcast_advertising/podcast-get-sponsor-leaderboard). # particle_podcast_get_bias_leaderboard Source: https://docs.particle.pro/mcp/tools/podcast_bias/podcast-get-bias-leaderboard Rank podcast publishers by political-bias metrics across their analyzed catalogs. In the **`podcast_bias`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_bias`. See [Tool sets & discovery](/mcp/tool-sets). Rank podcast publishers by political-bias metrics across their analyzed catalogs: most left/right leaning (average lean score), most political (share of political shows), most diverse / most monolithic (lean spread), or most analyzed (coverage). Publisher slugs feed [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher) (use `include: ["bias"]` there for one publisher's full bias profile). For publishers concentrated in one specific bias bucket, use [`particle_podcast_list_bias_publishers`](/mcp/tools/podcast_bias/podcast-list-bias-publishers). The per-podcast bias enum is already on every podcast result from [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve). ## Inputs | Field | Type | Required | Default | Description | | ------------------------ | --------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `metric` | string | yes | — | Ranking metric. `most_left_leaning` / `most_right_leaning` rank by average lean score; `most_political` by share of political podcasts; `most_diverse` / `most_monolithic` by lean spread; `most_analyzed` by coverage. | | `political_context` | string | no | — | Restrict the corpus to one political framework before aggregating. One of `US`, `UK`, `EU`, `CANADA`, `AUSTRALIA`, `INDIA`, `OTHER`, `UNKNOWN`. | | `min_analyzed_podcasts` | integer (1–500) | no | 5 | Only rank publishers with at least this many analyzed podcasts (aggregate signals are unreliable below it). | | `min_political_podcasts` | integer (1–500) | no | 5 | Only rank publishers with at least this many political podcasts. Enforced only for the lean and diversity metrics (`most_left_leaning`, `most_right_leaning`, `most_diverse`, `most_monolithic`); ignored for `most_political` and `most_analyzed`. | | `since` | string | no | — | Only analyses evaluated on or after this ISO 8601 timestamp. | | `until` | string | no | — | Only analyses evaluated before this ISO 8601 timestamp. | | `limit` | integer (1–50) | no | 10 | Publishers per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Publisher bias leaderboard — (N)` and a flat bullet list — one bullet per ranked publisher, formatted `# Publisher name (publisher-slug) — N analyzed, M political (P%), dominant , avg lean X.XX, lean stddev Y.YY` (the `political (P%)` share renders whenever the publisher has political podcasts; `dominant`/`avg lean` when a lean exists; `lean stddev` when the spread is defined — i.e. for the diversity metrics). When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`metric="most_right_leaning", limit=3`): ```markdown theme={"dark"} ## Publisher bias leaderboard — most_right_leaning (3) - #1 The Daily Wire (the-daily-wire) — 11 analyzed, 11 political (100%), dominant RIGHT, avg lean 0.71 - #2 Blaze Media (blaze-media) — 9 analyzed, 9 political (100%), dominant RIGHT, avg lean 0.64 - #3 Salem Media (salem-media) — 28 analyzed, 24 political (86%), dominant LEANS_RIGHT, avg lean 0.55 ``` The parenthesized `publisher-slug` feeds [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher). ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_bias_leaderboard { "metric": "most_left_leaning", "political_context": "US", "min_political_podcasts": 5, "limit": 25 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/bias/publishers/leaderboard`](/podcasts/publishers-bias). * For publishers concentrated in one bias bucket, use [`particle_podcast_list_bias_publishers`](/mcp/tools/podcast_bias/podcast-list-bias-publishers). * For one publisher's full bias profile, use [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher) with `include: ["bias"]`. # particle_podcast_list_bias_publishers Source: https://docs.particle.pro/mcp/tools/podcast_bias/podcast-list-bias-publishers Which publishers have the most podcasts in one political-bias bucket — by raw count or by share of catalog. In the **`podcast_bias`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_bias`. See [Tool sets & discovery](/mcp/tool-sets). Which publishers have the most podcasts in one political-bias bucket (e.g. `LEANS_RIGHT`, `NOT_POLITICAL`). Sort by raw count, or by share of the analyzed catalog to surface publishers whose output is concentrated in the bucket. Publisher slugs feed [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher). For cross-bucket metrics (most left/right leaning, most diverse), use [`particle_podcast_get_bias_leaderboard`](/mcp/tools/podcast_bias/podcast-get-bias-leaderboard). ## Inputs | Field | Type | Required | Default | Description | | ------------------- | --------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `result` | string | yes | — | Bias bucket to flip on — which publishers have the most podcasts with this result. One of `NOT_POLITICAL`, `EXTREME_LEFT`, `LEFT`, `LEANS_LEFT`, `CENTER`, `LEANS_RIGHT`, `RIGHT`, `EXTREME_RIGHT`. | | `political_context` | string | no | — | Restrict the corpus to one political framework. One of `US`, `UK`, `EU`, `CANADA`, `AUSTRALIA`, `INDIA`, `OTHER`, `UNKNOWN`. | | `min_count` | integer (1–500) | no | 1 | Only publishers with at least this many podcasts in the bucket. | | `sort` | string | no | `count` | `count` ranks by raw podcasts in the bucket; `share` ranks by the bucket's share of the analyzed catalog (surfaces concentrated publishers). | | `limit` | integer (1–50) | no | 10 | Publishers per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Publishers with podcasts (N)` and a flat bullet list — one bullet per publisher, formatted `Publisher name (publisher-slug) — N in bucket (X% of analyzed)` (the share suffix appears when nonzero). When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`result="LEANS_RIGHT", sort="share", limit=3`): ```markdown theme={"dark"} ## Publishers with LEANS_RIGHT podcasts (3) - The Daily Wire (the-daily-wire) — 9 in bucket (82% of analyzed) - Salem Media (salem-media) — 14 in bucket (50% of analyzed) - iHeartPodcasts (iheartpodcasts) — 31 in bucket (12% of analyzed) ``` The parenthesized `publisher-slug` feeds [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher). ## Example ```text theme={"dark"} Agent calls: particle_podcast_list_bias_publishers { "result": "NOT_POLITICAL", "sort": "count", "min_count": 5, "limit": 25 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/bias/{result}/publishers`](/podcasts/publishers-bias). * For cross-bucket metrics, use [`particle_podcast_get_bias_leaderboard`](/mcp/tools/podcast_bias/podcast-get-bias-leaderboard). * For one publisher's full bias profile, use [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher) with `include: ["bias"]`. # particle_podcast_get_guest Source: https://docs.particle.pro/mcp/tools/podcast_guests/podcast-get-guest A guest's podcast-appearance profile — lifetime stats plus optional appearances, per-podcast rollup, and suitability exposure. A guest's podcast-appearance profile: lifetime stats (appearances, distinct podcasts, first/last appearance) plus their most frequent podcasts. Guests are people — the same slug works with [`particle_person_get`](/mcp/tools/people/person-get) for the biographical profile. Request optional sections via `include`: `appearances` for the most recent episode appearances (episode and podcast slugs included for follow-up calls), `podcasts` for the per-podcast rollup, `suitability` for brand-suitability exposure across the podcasts they appear on. Returns `not_found` for people who exist but have never appeared on a podcast — use [`particle_person_get`](/mcp/tools/people/person-get) for those. ## Inputs | Field | Type | Required | Default | Description | | ------------ | -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `guest_slug` | string | yes | — | Person slug (e.g. `"sam-altman"`) from `particle_podcast_list_guests`, `particle_person_resolve`, or `particle_entity_resolve`. | | `include` | array of enums | no | `[]` | Optional response sections: `appearances` (most recent episode appearances with episode/podcast slugs), `podcasts` (per-podcast rollup of where they appear), `suitability` (brand-suitability exposure across the podcasts they appear on). Default response is the profile + lifetime stats. | ## Output A markdown document with the guest's name as an H2, then a `**Slug:**` row, a `**Current role:**` row (`Title, Company`) when known, and a `**Stats:**` row (`N appearances across M podcasts, last YYYY-MM-DD`). A `### Top podcasts` section always lists their most frequent shows. The following H3 sections appear when the matching `include` value was requested: * `### Podcasts` — the full per-podcast rollup, each `Podcast title (podcast-slug) — N appearances, last YYYY-MM-DD`. * `### Recent appearances` — `Episode title (episode-slug) on Podcast title (podcast-slug) — YYYY-MM-DD`. * `### Suitability exposure` — `Lifetime` and `Last 90 days` tier distributions (`SAFE N, LIMITED N, …`), plus a notes paragraph when present. The `podcast-slug` and `episode-slug` values round-trip into the podcast tools. Sample (`guest_slug="sam-altman", include=["podcasts"]`, truncated): ```markdown theme={"dark"} ## Sam Altman **Slug:** sam-altman **Current role:** CEO, OpenAI **Stats:** 38 appearances across 22 podcasts, last 2026-05-28 ### Top podcasts - All-In (all-in) — 6 appearances, last 2026-05-28 - Lex Fridman Podcast (lex-fridman-podcast) — 3 appearances, last 2026-03-12 ### Podcasts - All-In (all-in) — 6 appearances, last 2026-05-28 - Lex Fridman Podcast (lex-fridman-podcast) — 3 appearances, last 2026-03-12 - Hard Fork (hard-fork) — 2 appearances, last 2026-01-09 ``` ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_guest { "guest_slug": "sam-altman", "include": ["appearances", "suitability"] } ``` ## Related * REST equivalent: [`GET /v1/podcasts/guests/{id}`](/podcasts/guests). * For the biographical profile (bio, external links, role history), use [`particle_person_get`](/mcp/tools/people/person-get) with the same slug. * Discover guests with [`particle_podcast_list_guests`](/mcp/tools/podcast_guests/podcast-list-guests). # particle_podcast_list_guests Source: https://docs.particle.pro/mcp/tools/podcast_guests/podcast-list-guests Browse podcast guests across the catalog — the directory by lifetime appearances, or who's trending right now. Browse podcast guests across the catalog, in two opinionated modes: * **`directory`** (default): the guest directory ranked by lifetime appearances (guests with 2+ appearances). * **`trends`**: who's making the rounds right now — guests with appearances on 2+ distinct podcasts in the last 30 days, which surfaces cross-show press tours rather than show regulars. The press-tour shape is enforced by several baked filters: every in-window appearance must be on a different podcast (no stacking repeats on one show), each appearance needs 5+ minutes of identified speaking time, mononymous catch-all people are excluded, and the in-window rate must be at least a 2× spike over the guest's lifetime baseline. A guest matching the headline criteria but failing any of these will not appear. `topic_slug` narrows either mode to guests appearing on episodes about that topic. Guest identifiers **are** person handles — usually the canonical person slug, falling back to the encoded person ID when a slug hasn't been backfilled yet (both forms are accepted everywhere a person is taken). Feed them into [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest) for the appearance profile or [`particle_person_get`](/mcp/tools/people/person-get) for the person profile. This surface is deliberately minimal — only a topic filter and pagination. The directory uses lifetime appearance counts (floor of 2 appearances); trends uses a 30-day window with a 2+ distinct-podcast bar. The full filter surface lives on the REST API. ## Inputs | Field | Type | Required | Default | Description | | ------------ | -------------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | string | no | `directory` | What to return: `directory` (lifetime appearances) or `trends` (last-30-day cross-show press tours). | | `topic_slug` | string | no | — | Restrict to guests with appearances on episodes classified under this topic (slug from `particle_topic_browse`, e.g. `"technology/artificial-intelligence"`). | | `limit` | integer (1–50) | no | 20 | Guests per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Podcast guests — (N)` and a flat bullet list — one bullet per guest, formatted `**Name** (slug) — `. In `directory` mode `` is `N appearances across M podcasts, last YYYY-MM-DD`; in `trends` mode it is `N podcasts in the last 30 days, M lifetime appearances, last YYYY-MM-DD`. The parenthesized identifier is the canonical person slug when one has been backfilled, otherwise the encoded person ID — both forms are accepted everywhere a person is taken (guest\_slug, person\_slug, particle\_person\_get). When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`mode=trends`, `limit=3`): ```markdown theme={"dark"} ## Podcast guests — trends (3) - **Sam Altman** (sam-altman) — 4 podcasts in the last 30 days, 38 lifetime appearances, last 2026-05-28 - **Cathie Wood** (cathie-wood) — 3 podcasts in the last 30 days, 21 lifetime appearances, last 2026-05-26 - **Marc Andreessen** (marc-andreessen) — 2 podcasts in the last 30 days, 15 lifetime appearances, last 2026-05-22 ``` ## Example ```text theme={"dark"} Agent calls: particle_podcast_list_guests { "mode": "directory", "topic_slug": "technology/artificial-intelligence", "limit": 25 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/guests`](/podcasts/guests) for `mode="directory"`; [`GET /v1/podcasts/guests/trends`](/podcasts/guests#trending-guests) for `mode="trends"`. * For a single guest's appearance profile, use [`particle_podcast_get_guest`](/mcp/tools/podcast_guests/podcast-get-guest). * Browse the topic taxonomy with [`particle_topic_browse`](/mcp/tools/topics/topic-browse). # particle_podcast_get_publisher Source: https://docs.particle.pro/mcp/tools/podcast_publishers/podcast-get-publisher A podcast publisher's profile — name, slug, catalog size, and optional shows, bias profile, and suitability profile. In the **`podcast_publishers`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_publishers`. See [Tool sets & discovery](/mcp/tool-sets). A podcast publisher's profile: name, slug, and catalog size. Request optional sections via `include`: `podcasts` for the publisher's shows (podcast slugs feed the podcast tools), `bias` for the political-bias profile across the analyzed catalog (coverage, political share, lean, distributions), `suitability` for the IAB/GARM brand-suitability profile (tier breakdown, category exposure, top concerns). Publisher slugs come from podcast payloads and from the corpus-wide views: [`particle_podcast_get_bias_leaderboard`](/mcp/tools/podcast_bias/podcast-get-bias-leaderboard), [`particle_podcast_list_bias_publishers`](/mcp/tools/podcast_bias/podcast-list-bias-publishers), [`particle_podcast_get_suitability_leaderboard`](/mcp/tools/podcast_suitability/podcast-get-suitability-leaderboard), and [`particle_podcast_list_suitability_category_publishers`](/mcp/tools/podcast_suitability/podcast-list-suitability-category-publishers). ## Inputs | Field | Type | Required | Default | Description | | ---------------- | -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `publisher_slug` | string | yes | — | Publisher slug (e.g. `"iheartpodcasts"`, `"goalhanger"`, `"bbc-radio-4"`) or encoded ID. Publisher slugs appear on podcast payloads and in the bias/suitability leaderboards. | | `include` | array of enums | no | `[]` | Optional response sections: `podcasts` (the publisher's shows with slugs), `bias` (political-bias profile across the analyzed catalog — premium-grade data), `suitability` (IAB/GARM brand-suitability profile across the analyzed catalog — premium-grade data). Default response is the compact profile. | ## Output A markdown document with the publisher's name as an H2, a `**Slug:**` row, and a `**Podcasts:**` count. The following H3 sections appear when the matching `include` value was requested: * `### Shows` — one bullet per show, `Title (podcast-slug) — bias , suitability ` (bias/suitability suffixes appear only when assessed). * `### Bias profile` — `Analyzed N of M podcasts`, `Political N podcasts (X%)`, `Lean (avg score X.XX)`, `Last evaluated YYYY-MM-DD`. * `### Suitability profile` — `Analyzed N of M podcasts`, `Tiers SAFE n, LIMITED n, SENSITIVE n, UNSAFE n`, then a `Concern` bullet per top concern (`code (N high-risk podcasts)`). Sample (`publisher_slug="goalhanger", include=["bias"]`, truncated): ```markdown theme={"dark"} ## Goalhanger **Slug:** goalhanger **Podcasts:** 14 ### Bias profile - **Analyzed:** 12 of 14 podcasts - **Political:** 9 podcasts (75%) - **Lean:** CENTER (avg score 0.04) - **Last evaluated:** 2026-05-20 ``` ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_publisher { "publisher_slug": "iheartpodcasts", "include": ["podcasts", "suitability"] } ``` ## Related * REST equivalents: [`GET /v1/podcasts/publishers/{id}`](/podcasts/publishers), [`/bias`](/podcasts/publishers-bias), [`/suitability`](/podcasts/publisher-suitability). * For corpus-wide rankings, use [`particle_podcast_get_bias_leaderboard`](/mcp/tools/podcast_bias/podcast-get-bias-leaderboard) and [`particle_podcast_get_suitability_leaderboard`](/mcp/tools/podcast_suitability/podcast-get-suitability-leaderboard). # particle_podcast_get_rankings Source: https://docs.particle.pro/mcp/tools/podcast_rankings/podcast-get-rankings Podcast chart rankings from Apple Podcasts and Spotify — current charts, biggest movers, and ranking history. Podcast chart rankings from Apple Podcasts and Spotify, in three modes: * **`chart`** (default): the current chart for a source/country/category slot, or — with `podcast_slug` — every chart slot that podcast currently holds. * **`movers`**: the biggest rank changes over `window_days` (risers, fallers, debuts, exits). * **`history`**: past snapshots for a chart slot, or — with `podcast_slug` — one podcast's chart history over time. Each row carries the matched `podcast_slug` when the chart entry is in the catalog — feed it into [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) or any podcast tool. For a single podcast's at-a-glance chart presence, `particle_podcast_resolve` with `include: ["rankings"]` is one call instead of two. ## Inputs | Field | Type | Required | Default | Description | | --------------- | --------------- | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | string | no | `chart` | What to return: `chart`, `movers`, or `history`. | | `podcast_slug` | string | no | — | Podcast slug, internal ID, or numeric iTunes ID. With `mode=chart`: that podcast's current chart appearances across every slot. With `mode=history`: that podcast's chart history. | | `source` | string | no | `apple`\* | Ranking source platform. One of `apple`, `spotify`. | | `country` | string | no | `us`\* | ISO 3166-1 alpha-2 country code (e.g. `"us"`, `"gb"`, `"jp"`). | | `category_slug` | string | no | — | Category slug (e.g. `"comedy"`, `"business"`). Omit for the overall chart. | | `window_days` | integer (1–30) | no | 1 | **Mode=movers only**: comparison window in days (`1` = vs. yesterday). | | `change` | string | no | `all` | **Mode=movers only**: filter by change type. One of `all`, `up`, `down`, `new`, `exit`. | | `since` | string | no | — | **Mode=history only**: only snapshots captured on or after this ISO 8601 timestamp. | | `until` | string | no | — | **Mode=history only**: only snapshots captured on or before this ISO 8601 timestamp. | | `limit` | integer (1–100) | no | 25 | Rows per page. | | `cursor` | string | no | — | Opaque pagination cursor. Not supported by `mode=movers`. | \* The `apple`/`us` defaults apply to the slot-wide reads (`chart` and `history` without `podcast_slug`, and `movers`). The per-podcast views — `chart` or `history` with `podcast_slug` and no slot filters — span **all** sources and countries; set `source`/`country` explicitly to narrow them. ## Output A markdown document with `## Podcast rankings — (N rows)` and a flat bullet list — one bullet per chart entry, formatted `# Show name (podcast-slug) — `, where `` is `source/country/category`. In `movers` mode each bullet appends `[change ±delta]`. In `history` mode each bullet appends `@ YYYY-MM-DD` (the snapshot date). The `podcast-slug` parenthetical appears only when the chart entry matched a catalog podcast — long-tail and international entries often haven't, so the show name is the universal identity. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`mode=movers`, `window_days=7`, `change=up`, `limit=3`): ```markdown theme={"dark"} ## Podcast rankings — movers (3 rows) - #2 The Daily (the-daily) — apple/us [up +6] - #5 Hard Fork (hard-fork) — apple/us [up +3] - #9 Acquired (acquired) — apple/us [up +11] ``` ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_rankings { "mode": "chart", "source": "apple", "country": "us", "category_slug": "business", "limit": 25 } ``` One podcast's history over a window: ```text theme={"dark"} Agent calls: particle_podcast_get_rankings { "mode": "history", "podcast_slug": "all-in", "since": "2026-01-01" } ``` ## Related * REST equivalents by mode: [`GET /v1/podcasts/rankings`](/podcasts/rankings) or `GET /v1/podcasts/{id}/rankings` (`chart`), `GET /v1/podcasts/rankings/movers` (`movers`), `GET /v1/podcasts/rankings/history` or `GET /v1/podcasts/{id}/rankings/history` (`history`). * For one podcast's current chart positions inline, use [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) with `include: ["rankings"]`. # particle_podcast_get_ratings Source: https://docs.particle.pro/mcp/tools/podcast_ratings/podcast-get-ratings Listener-review ratings for one podcast — aggregate summary, per-platform breakdown, sentiment, and recent reviews. In the **`podcast_ratings`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_ratings`. See [Tool sets & discovery](/mcp/tool-sets). Listener-review ratings for one podcast: the aggregate summary (average stars, rating count, per-platform breakdown, and an LLM-generated sentiment narrative when available) plus the most recent individual reviews. Filter the review list by `platform_slug`, `min_stars`, or a `since`/`until` window; the summary always reflects the full corpus. For just the headline numbers on several podcasts at once, [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) with `include: ["ratings_summary"]` is cheaper. ## Inputs | Field | Type | Required | Default | Description | | --------------- | -------------- | -------- | ------- | ----------------------------------------------------------------------------------- | | `podcast_slug` | string | yes | — | Podcast slug, internal ID, or numeric iTunes ID. | | `platform_slug` | string | no | — | Restrict the rating list to one platform (e.g. `"apple"`). Omit for every platform. | | `min_stars` | integer (1–5) | no | — | Lower bound on the star rating (inclusive). | | `since` | string | no | — | Only ratings posted on or after this ISO 8601 date. | | `until` | string | no | — | Only ratings posted on or before this ISO 8601 date. | | `limit` | integer (1–50) | no | 10 | Ratings per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Ratings for ` and an `**Overall:**` row (`X.XX stars across N ratings`), then one `BulletKV` per `(platform, locale)` aggregate (`platform/locale: X.XX stars across N ratings`). A `### Listener sentiment` section carries the narrative paragraph when present. A `### Recent reviews` section lists each review as `N★ Title — Author (YYYY-MM-DD)` with the review body indented underneath. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`podcast_slug="all-in", limit=2`): ```markdown theme={"dark"} ## Ratings for all-in **Overall:** 4.71 stars across 18432 ratings - **apple/us:** 4.73 stars across 15210 ratings - **spotify/us:** 4.62 stars across 3222 ratings ### Listener sentiment Listeners praise the besties' candor and tech-investing insight; recurring criticism centers on political tangents. ### Recent reviews - 5★ Best tech podcast — degen_investor (2026-05-28) The only show that actually explains the market to me. - 2★ Too much politics lately — longtime_listener (2026-05-26) Used to be about startups, now it's all hot takes. ``` ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_ratings { "podcast_slug": "all-in", "platform_slug": "apple", "min_stars": 1, "limit": 20 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/{id}/ratings`](/podcasts/ratings). * For the headline numbers inline on resolve, use [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) with `include: ["ratings_summary"]`. # particle_podcast_get_suitability_leaderboard Source: https://docs.particle.pro/mcp/tools/podcast_suitability/podcast-get-suitability-leaderboard Rank podcast publishers by IAB/GARM brand-suitability composition across their analyzed catalogs. In the **`podcast_suitability`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_suitability`. See [Tool sets & discovery](/mcp/tool-sets). Rank podcast publishers by IAB/GARM brand-suitability composition across their analyzed catalogs: `safest` (highest SAFE share), `riskiest` (highest UNSAFE share), `most_placeable` (highest SAFE+LIMITED share — the broadly buyable slice), or `most_analyzed` (coverage). Publisher slugs feed [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher) (use `include: ["suitability"]` there for one publisher's full profile). For exposure to one specific GARM category, use [`particle_podcast_list_suitability_category_publishers`](/mcp/tools/podcast_suitability/podcast-list-suitability-category-publishers). A single podcast's breakdown is [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) with `include: ["suitability"]`. ## Inputs | Field | Type | Required | Default | Description | | ----------------------- | ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `metric` | string | no | `safest` | How to rank publishers: `safest` (SAFE share), `riskiest` (UNSAFE share), `most_placeable` (combined SAFE+LIMITED share), `most_analyzed` (absolute analyzed-podcast count). | | `min_analyzed_podcasts` | integer (1–1000) | no | 5 | Exclude publishers with fewer analyzed podcasts (guards against tiny catalogs dominating share metrics). | | `limit` | integer (1–50) | no | 10 | Publishers per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Publisher suitability leaderboard — (N)` and a flat bullet list — one bullet per ranked publisher, formatted `# Publisher name (publisher-slug) — N analyzed; SAFE n, LIMITED n, SENSITIVE n, UNSAFE n; metric X.XX`. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`metric="safest", limit=3`): ```markdown theme={"dark"} ## Publisher suitability leaderboard — safest (3) - #1 BBC Radio 4 (bbc-radio-4) — 22 analyzed; SAFE 20, LIMITED 2, SENSITIVE 0, UNSAFE 0; metric 0.91 - #2 NPR (npr) — 41 analyzed; SAFE 35, LIMITED 5, SENSITIVE 1, UNSAFE 0; metric 0.85 - #3 Goalhanger (goalhanger) — 14 analyzed; SAFE 11, LIMITED 3, SENSITIVE 0, UNSAFE 0; metric 0.79 ``` The parenthesized `publisher-slug` feeds [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher). ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_suitability_leaderboard { "metric": "most_placeable", "min_analyzed_podcasts": 10, "limit": 25 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/suitability/publishers/leaderboard`](/podcasts/publisher-suitability). * For exposure to one specific GARM category, use [`particle_podcast_list_suitability_category_publishers`](/mcp/tools/podcast_suitability/podcast-list-suitability-category-publishers). * For one publisher's full profile, use [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher) with `include: ["suitability"]`. # particle_podcast_list_suitability_category_publishers Source: https://docs.particle.pro/mcp/tools/podcast_suitability/podcast-list-suitability-category-publishers Which publishers' catalogs are most (or least) exposed to one IAB/GARM brand-safety category. In the **`podcast_suitability`** category, which is **opt-in** — it is callable by name on any connection, but only advertised on `tools/list` when selected via `?include=podcast_suitability`. See [Tool sets & discovery](/mcp/tool-sets). Which publishers' catalogs are most (or least) exposed to one IAB/GARM brand-safety category — e.g. misinformation, debated social issues, illegal drugs/alcohol/tobacco. Tighten with `min_prevalence` (how often the content appears) and `treatment` (how it's handled — `GLAMORIZING` is the worst). Each entry carries example podcast slugs anchoring the shares to recognizable shows. Publisher slugs feed [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher); for cross-category tier composition use [`particle_podcast_get_suitability_leaderboard`](/mcp/tools/podcast_suitability/podcast-get-suitability-leaderboard). ## Inputs | Field | Type | Required | Default | Description | | ----------------------- | ---------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `code` | string | yes | — | GARM brand-safety category code to pivot on. One of `adult_sexual`, `arms_ammunition`, `crime_harmful_acts`, `death_injury_military_conflict`, `online_piracy`, `hate_speech_aggression`, `obscenity_profanity`, `illegal_drugs_alcohol_tobacco`, `spam_harmful`, `terrorism`, `debated_social_issues`, `misinformation`. | | `direction` | string | no | `most_exposed` | `most_exposed` ranks publishers by the share of their analyzed catalog exposed to the category; `least_exposed` inverts it. | | `min_prevalence` | string | no | `INCIDENTAL` | Only count a podcast as exposed at or above this prevalence (default `INCIDENTAL` — any non-NONE). One of `INCIDENTAL`, `OCCASIONAL`, `FREQUENT`, `PERVASIVE`. | | `treatment` | string | no | — | Only count exposures with this treatment — e.g. `GLAMORIZING` to surface the worst handling. One of `DOCUMENTARY`, `EDITORIAL`, `PROMOTIONAL`, `GLAMORIZING`. | | `min_analyzed_podcasts` | integer (1–1000) | no | 5 | Exclude publishers with fewer analyzed podcasts. | | `limit` | integer (1–50) | no | 10 | Publishers per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Publisher exposure to (N)` and a flat bullet list — one bullet per ranked publisher, formatted `# Publisher name (publisher-slug) — N of M analyzed exposed (X%), K high-risk — e.g. slug-a, slug-b`. The example podcast slugs anchor the shares to recognizable shows and round-trip into the podcast tools. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Sample (`code="misinformation", limit=3`): ```markdown theme={"dark"} ## Publisher exposure to misinformation (3) - #1 Infowars Network (infowars-network) — 6 of 7 analyzed exposed (86%), 5 high-risk — e.g. the-alex-jones-show, war-room - #2 The Daily Wire (the-daily-wire) — 4 of 11 analyzed exposed (36%), 2 high-risk — e.g. the-ben-shapiro-show - #3 iHeartPodcasts (iheartpodcasts) — 18 of 260 analyzed exposed (7%), 6 high-risk — e.g. stuff-they-dont-want-you-to-know ``` The parenthesized `publisher-slug` feeds [`particle_podcast_get_publisher`](/mcp/tools/podcast_publishers/podcast-get-publisher). ## Example ```text theme={"dark"} Agent calls: particle_podcast_list_suitability_category_publishers { "code": "illegal_drugs_alcohol_tobacco", "min_prevalence": "OCCASIONAL", "treatment": "GLAMORIZING", "limit": 25 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/suitability/categories/{code}/publishers`](/podcasts/publisher-suitability). * For cross-category tier composition, use [`particle_podcast_get_suitability_leaderboard`](/mcp/tools/podcast_suitability/podcast-get-suitability-leaderboard). * For one podcast's breakdown, use [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) with `include: ["suitability"]`. # particle_podcast_find_mentions Source: https://docs.particle.pro/mcp/tools/podcasts/podcast-find-mentions Find dialogue lines where a specific person or company is named in podcast transcripts. Find dialogue lines where a specific person or company is named in podcast transcripts. Use this for the "every line about X" workflow. For dialogue that *discusses* a topic (paraphrase-tolerant or BM25), use [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts) instead — that one ranks segments by relevance to a free-text query, not by who's named. ## Two response modes **`format="summary"` (default — wide scan).** Returns up to `limit` episodes (reverse-chronological), each with metadata plus the first 10 mention-only lines (just the lines naming the entity, no surrounding dialogue). Use this to see *what's been said across episodes* and decide which episodes are worth reading in full. Paginate older episodes with `cursor`. **`format="detail"` (narrow drill-in).** Requires `episode_slug`. Returns the full mention windows with `context_lines` of surrounding dialogue around each mention. Pass one slug for a single episode, or up to **10 comma-separated** slugs (e.g. `episode_slug="all-in-200,all-in-201,all-in-202"`) to multi-get several episodes in one call. `limit`/`cursor` don't apply. ### Workflow * **No specific episode in mind:** call `format="summary"` first to scan, then call `format="detail"` with the slug(s) of the episodes worth reading in full. For most questions (sentiment, recurring themes, who said what when), summary alone has enough signal and the second call isn't needed. * **Already have the episode slug** (e.g. user mentioned the episode by name, or you got it from `particle_podcast_get_episode` / `particle_podcast_search_transcripts`): skip summary entirely and call `format="detail"` with `episode_slug` directly. ## Inputs One of `person_slug` or `company_slug` is required. Resolve a name to a slug first with [`particle_person_resolve`](/mcp/tools/people/person-resolve), [`particle_entity_resolve`](/mcp/tools/people/entity-resolve), or [`particle_company_resolve`](/mcp/tools/companies/company-resolve). | Field | Type | Required | Default | Description | | --------------- | ------------------------- | ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `format` | `"summary"` \| `"detail"` | no | `"summary"` | Response shape. See "Two response modes" above. | | `person_slug` | string | one of | — | Person slug from `particle_person_resolve` or `particle_entity_resolve` (e.g. `"sam-altman"`). Also accepts a bare entity slug for non-person entities like places. | | `company_slug` | string | one of | — | Company slug, domain, or canonical ID (e.g. `"nvidia"` or `"nvidia.com"`). Resolves to the company's linked entity. | | `episode_slug` | string | yes for `detail` | — | One slug for single drill-in, or up to 10 comma-separated slugs for a multi-episode drill-in in one call. In `summary` mode, optional filter to one episode. | | `podcast_slug` | string | no | — | Restrict mentions to a single podcast by slug, ID, or numeric iTunes ID. | | `role` | enum | no | — | Constrain how the entity participates. One of: `guest`, `host`, `panelist`, `correspondent`, `mention`. | | `since` | string | no | — | Only episodes published on or after this ISO 8601 date (e.g. `2025-01-01`). | | `until` | string | no | — | Only episodes published on or before this ISO 8601 date. | | `context_lines` | integer (1–20) | no | 2 | Surrounding dialogue lines around each mention. **Detail mode only** — ignored in summary. | | `limit` | integer (1–50) | no | 10 | Episodes per page. **Summary mode only**. | | `cursor` | string | no | — | Opaque pagination cursor. **Summary mode only**. | ## Output A markdown document with `## Mentions of (N episodes, format=)` and one `### Episode title` section per episode. Each section carries bulleted KV rows: `- **Podcast:**`, `- **Podcast slug:**`, `- **Published:**`, `- **Episode slug:**`, `- **Mentions:**`. In **summary** mode, each episode is followed by a flat bullet list — one bullet per mention, formatted `@ s **Speaker:** text`. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended; pass that value back as `cursor` to fetch the next page. In **detail** mode, each episode is followed by one bullet per mention window (`Segment title (TYPE) @ start–end`), then indented dialogue lines underneath. The line that actually names the entity has the entire `Speaker: text` wrapped in bold; surrounding context lines render as `**Speaker:** text` (speaker label bold, text plain). Sample (summary, `person_slug="marc-andreessen", limit=2`): ```markdown theme={"dark"} ## Mentions of Marc Andreessen (2 episodes, format=summary) ### The Sunday Interview: Governing Without Accountability … - **Podcast:** Straight White American Jesus - **Published:** 2026-05-03T13:15:00Z - **Episode slug:** the-sunday-interview-governing-without-accountability-silicon-valleys-ideology - **Mentions:** 5 - @ 719s **Annika Brockschmidt:** And one of the people who really likes to claim that, … Marc Andreessen- - @ 1098s **Annika Brockschmidt:** I f- I especially find the stuff that Marc Andreessen says out loud into microphones … ### Why Most of What You're Hearing About AI Is Wrong - **Podcast:** DarrenDaily On-Demand - **Published:** 2026-05-01T10:00:00Z - **Episode slug:** why-most-of-what-youre-hearing-about-ai-is-wrong - **Mentions:** 1 - @ 155s **Darren Hardy:** Marc Andreessen calls it the silver bullet excuse for layoffs driven by pandemic over-hiring. --- **Cursor:** s.1hwwoMp2yqmPExv6N5kGy5OYdb8kKihxYYWJ ``` Sample (detail, drilling into one of those episodes): ```markdown theme={"dark"} ## Mentions of Marc Andreessen (1 episodes, format=detail) ### Why Most of What You're Hearing About AI Is Wrong - **Podcast:** DarrenDaily On-Demand - **Published:** 2026-05-01T10:00:00Z - **Episode slug:** why-most-of-what-youre-hearing-about-ai-is-wrong - **Mentions:** 1 - AI as layoff scapegoat and market incentives (TOPIC_DISCUSSION) @ 140s–183s **Darren Hardy:** When Elon Musk bought it, he gutted 80% of the staff … **Darren Hardy: Marc Andreessen calls it the silver bullet excuse for layoffs driven by pandemic over-hiring.** **Darren Hardy:** So let me be clear about this, okay? AI is not what's killing the job market, yet at least. ``` ## Example ```text theme={"dark"} # Wide scan, then drill in. Agent calls: particle_podcast_find_mentions { "company_slug": "openai", "podcast_slug": "all-in", "since": "2025-11-01", "limit": 20 } → reads "- **Episode slug:**" rows for episodes worth reading in full → e.g. "all-in-200", "all-in-201" Agent calls: particle_podcast_find_mentions { "format": "detail", "company_slug": "openai", "episode_slug": "all-in-200,all-in-201" } → returns full mention windows with context for both episodes in one call ``` ## Related * REST equivalent: [`GET /v1/podcasts/mentions`](/podcasts/mentions). * For ranked dialogue *about* a topic (not just lines naming a person), use [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts). * For episode-level discovery without dialogue, use [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes). # particle_podcast_get_episode Source: https://docs.particle.pro/mcp/tools/podcasts/podcast-get-episode Bundled overview of one episode — speakers, top entities, segment/clip counts, and optional transcript, segments, entities, clips, and topics. Return a bundled overview of one podcast episode: title, podcast, speakers (with entity slugs), top mentioned entities, and segment/clip counts. By default the response is lean — counts plus the top mentioned entities. Request optional sections via `include`: `segments` for the structural outline with timestamps, `entities` for the complete entity list (instead of the top 20), `clips` for engagement-ranked highlight clips, `topics` for topic classifications with slugs, or `transcript` for the dialogue transcript (narrow it by speaker or time range via `transcript_speaker` / `transcript_start` / `transcript_end` — full transcripts are large). This is also where a known episode's **full clip list** lives — `include: ["clips"]`. There is no separate clip-listing tool; relevant clips for a *search* arrive inline on [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts) matches. For "every line about X in this episode" use [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) with `episode_slug` instead — that returns the dialogue around each mention with `is_mention` flags. ## Inputs | Field | Type | Required | Default | Description | | -------------------- | -------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `episode_slug` | string | yes | — | Episode slug or canonical ID. | | `include` | array of enums | no | `[]` | Optional response sections: `transcript` (bounded dialogue transcript — large for long episodes; narrow with the `transcript_*` sub-params), `segments` (structural outline with timestamps), `entities` (complete mentioned-entity list instead of the top 20), `clips` (engagement-ranked highlight clips), `topics` (topic classifications with slugs). Request only what you need. | | `transcript_format` | string | no | `text` | Transcript format for `include: ["transcript"]`. One of `dialogue`, `text`, `srt`. | | `transcript_speaker` | string | no | — | Filter the transcript to one speaker (name or entity slug). | | `transcript_start` | number | no | — | Transcript start clip in seconds. | | `transcript_end` | number | no | — | Transcript end clip in seconds. | The old boolean flags `include_transcript`, `include_full_segments`, and `include_full_entities` are gone — pass the matching value in the `include` array instead (`["transcript"]`, `["segments"]`, `["entities"]`). The `transcript_*` sub-parameters are unchanged. ## Output A markdown document with the episode title as an H2, then KV lines (`**Podcast:**` as `Title (slug)`, `**Published:**`, `**Episode slug:**`, `**Duration:**`, `**Counts:**` summarising segments/clips/entities), then a paragraph description. The following H3 sections appear when the underlying data is present: * `### Speakers` — bullets of `Name (role)` (slug appended as `— slug: ` when linked). * `### Top entities` — bullets of `Name (slug) — salience X.XXX, N occurrences`. Capped at 20 unless `include` contains `entities`. * `### Topics` — bullets of `Name — slug: `. Only when `include` contains `topics`. The slug round-trips into [`particle_topic_browse`](/mcp/tools/topics/topic-browse) and `topic_slug` filters. * `### Segments` — only when `include` contains `segments`. Bullets formatted `#N TYPE — Title @ Xs–Ys`. * `### Clips` — only when `include` contains `clips`. Bullets formatted `Title (clip-id) — TYPE, engagement N @ Xs–Ys`. * `### Transcript ()` — only when `include` contains `transcript`. Body is the raw transcript text/SRT, or for `dialogue` format a series of `**Speaker:** text` lines. Sample (`episode_slug="why-are-we-all-so-stressed", include=["segments","topics"]`, truncated): ```markdown theme={"dark"} ## Why are we all so stressed? **Podcast:** 6 Minute English (6-minute-english) **Published:** 2026-04-23 **Episode slug:** why-are-we-all-so-stressed **Duration:** 402s **Counts:** 9 segments, 0 clips, 4 entities Worrying about exams, work or climate change in the news. … ### Speakers - Neil (HOST) - Becca (HOST) ### Top entities - Claudia Hammond (claudia-hammond) — salience 0.875, 15 occurrences - English (english) — salience 0.375, 11 occurrences ### Topics - Mental Health — slug: society/mental-health - Stress Management — slug: society/mental-health/stress-management ### Segments - #1 INTRO — Show and series introduction @ 0s–41s - #2 PERSONAL_BANTER — Hosts share what makes them stressed @ 42s–94s - #3 TOPIC_DISCUSSION — Defining overwhelm and burnout @ 95s–150s ``` ## Example ```text theme={"dark"} Agent calls: particle_podcast_get_episode { "episode_slug": "whcd-shooting-aftermath-musk-and-altman-face-off", "include": ["segments", "transcript"], "transcript_format": "dialogue", "transcript_speaker": "kara-swisher" } ``` Pulling a known episode's full highlight-clip list: ```text theme={"dark"} Agent calls: particle_podcast_get_episode { "episode_slug": "all-in-200", "include": ["clips"] } ``` ## Related * REST equivalents: [`GET /v1/podcasts/episodes/{id}`](/podcasts/episodes), [`/transcript`](/podcasts/transcripts), [`/segments`](/podcasts/segments-and-clips), [`/entities`](/podcasts/episodes#entities). * For dialogue around a specific entity in this episode, use [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) with `episode_slug`. * For ranked dialogue across the catalog, use [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts). # particle_podcast_list_episodes Source: https://docs.particle.pro/mcp/tools/podcasts/podcast-list-episodes Filter-driven episode discovery across the catalog — by podcast, person, company, language, date range, duration, or transcript availability. List episodes across the catalog with rich filters: by podcast, person, company, language, date range, duration, or transcript availability. Use this for episode-level discovery when you only need metadata (title, duration, speakers, counts). For dialogue around a person in any episode, use [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions). For ranked retrieval by topic, use [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts). ## Inputs | Field | Type | Required | Default | Description | | ------------------ | -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `podcast_slug` | string | no | — | Podcast slug, internal ID, or numeric iTunes ID. Restrict to one podcast. | | `person_slug` | string | no | — | Person slug from `particle_person_resolve` or `particle_entity_resolve` (e.g. `"sam-altman"`). Episodes featuring or mentioning the person. Also accepts a bare entity slug for non-person entities like places. | | `company_slug` | string | no | — | Company slug, domain, or ID. Resolves to the linked entity. | | `role` | string | no | — | Role filter when `person_slug` or `company_slug` is set. One of: `guest`, `host`, `panelist`, `correspondent`, `mention`. | | `published_after` | string | no | — | ISO 8601 date or date-time. | | `published_before` | string | no | — | ISO 8601 date or date-time. | | `language` | string | no | — | ISO 639-1 language code (e.g. `"en"`). | | `has_transcript` | boolean | no | `false` | When `true`, return only episodes with a completed transcript. `false` or omitted means no transcript filter. | | `min_duration` | integer (≥0) | no | — | Minimum episode duration in seconds. | | `max_duration` | integer (≥0) | no | — | Maximum episode duration in seconds. | | `limit` | integer (1–50) | no | 10 | Episodes per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Episodes (N)` and one `### Episode title` section per episode. Each section carries bulleted KV rows: `- **Podcast:**`, `- **Published:**`, `- **Episode slug:**`, `- **Duration:**`, `- **Counts:**` (segments / clips / entities), and `- **Speakers:**`. The Speakers row is a comma-separated list with up to 6 names; entries with linked entities render as `Name (entity_slug)`. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended; pass that value back as `cursor` to fetch the next page. Sample (`podcast_slug="6-minute-english", limit=2`): ```markdown theme={"dark"} ## Episodes (2) ### Should we eat ultra-processed food? - **Podcast:** 6 Minute English - **Published:** 2026-04-30 - **Episode slug:** should-we-eat-ultra-processed-food - **Duration:** 402s - **Counts:** 12 segments, 0 clips, 5 entities - **Speakers:** Hannah Gelbart (hannah-gelbart), Pippa, speaker_2, speaker_3, Phil, Annabel Rackham (annabel-rackham) ### Why are we all so stressed? - **Podcast:** 6 Minute English - **Published:** 2026-04-23 - **Episode slug:** why-are-we-all-so-stressed - **Duration:** 402s - **Counts:** 9 segments, 0 clips, 4 entities - **Speakers:** Iqra Farooq, Neil, Becca, Claudia Hammond (claudia-hammond) --- **Cursor:** r.4gfFC6 ``` The string after `- **Episode slug:**` is what you pass back as `episode_slug` to [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) or [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions). ## Example ```text theme={"dark"} Agent calls: particle_podcast_list_episodes { "person_slug": "marc-andreessen", "role": "guest", "published_after": "2025-01-01", "has_transcript": true, "limit": 20 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/episodes`](/podcasts/episodes). * For per-episode dialogue, drill into [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) or [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) with the returned `episode_slug`. # particle_podcast_resolve Source: https://docs.particle.pro/mcp/tools/podcasts/podcast-resolve Find a podcast by title, slug, iTunes ID, or RSS URL; returns slug, episode count, bias, speakers, and optional hydrations. Find a podcast by free-text title, exact slug, iTunes ID, or RSS feed URL. Returns slug, title, episode count, bias, and the top recurring speakers (with entity slugs). Use the slug as the agent-facing handle to feed into other podcast tools. With all identifiers omitted, returns the most popular podcasts (popularity-ranked, not recency) — useful for browsing the catalog when you don't have a name in mind. Narrow free-text browsing with `topic_slug` (topic concentration, descendants included), `suitability_tier`, or `min_popularity` (global popularity percentile over charting podcasts). Optional hydrations attach extra data to each result in the same call: * `include: ["external_links"]`: third-party platform presences (Apple Podcasts, Spotify, YouTube, X, publisher website, etc.) with resolved URLs and audience metrics. * `include: ["suitability"]`: per-category brand-suitability breakdown (12 [IAB Tech Lab Brand Safety & Suitability categories](/podcasts/suitability) with prevalence, treatment, derived risk level, reasoning, and evidence excerpts). The high-level `suitability_tier` enum (`SAFE` / `LIMITED` / `SENSITIVE` / `UNSAFE`) is rendered on every result without opt-in. * `include: ["ratings_summary"]`: listener-review aggregate (average stars, count, per-platform breakdown). For the full review list use [`particle_podcast_get_ratings`](/mcp/tools/podcast_ratings/podcast-get-ratings). * `include: ["bias"]`: full political-bias analysis. The high-level `bias` enum is always rendered without opt-in. * `include: ["rankings"]`: current chart positions across sources/countries/categories. For movers and history use [`particle_podcast_get_rankings`](/mcp/tools/podcast_rankings/podcast-get-rankings). * `include: ["format"]`: the show's [format profile](/podcasts/format) — how often episodes feature guests, detected production formats (`interview`, `panel`, `call_in`, `solo_narrated`), ad and video presence, episode-length distribution, publishing cadence, and the publish-day pattern. * `recent_episodes: N`: inline this many of each result's most recent episodes (`episode_slug`, `title`, `published_at`, `duration_seconds`) — skip the follow-up `particle_podcast_list_episodes` call when you only need the most recent tail. ## Inputs | Field | Type | Required | Default | Description | | ------------------ | -------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | no | — | Free-text search across podcast titles and descriptions (case-insensitive partial match). Omit to fall back to the most recently updated podcasts. | | `slug` | string | no | — | Exact slug match for a known handle (e.g. `"all-in"`). | | `itunes_id` | string | no | — | Numeric Apple Podcasts / iTunes ID (e.g. `"1502871393"`). Resolves directly to the matching podcast. | | `rss_url` | string | no | — | Canonical RSS feed URL. Resolves directly to the matching podcast. | | `topic_slug` | string | no | — | Filter candidates by topic (slug from `particle_topic_browse`, e.g. `"technology/artificial-intelligence"`). Matches podcasts where the topic — or any descendant — accounts for a meaningful share of episodes, ranked by concentration. | | `suitability_tier` | string | no | — | Filter candidates by brand-suitability tier. One of `SAFE`, `LIMITED`, `SENSITIVE`, `UNSAFE`. Podcasts without a suitability analysis are excluded when set. | | `min_popularity` | number (0–1] | no | `0` | Restrict candidates to podcasts whose global popularity percentile is at least this value. Popularity is a `cume_dist` ranking over currently-charting podcasts; non-charting podcasts are excluded when set. Omit or `0` to disable. | | `limit` | integer (1–25) | no | 5 | Maximum candidates to return. | | `include` | array of enums | no | `[]` | Optional non-parameterized hydrations to attach to each result: `external_links`, `suitability`, `ratings_summary`, `bias`, `rankings`, `format`. The high-level `suitability_tier` and `bias` enums are always rendered without opt-in. | | `recent_episodes` | integer (0–25) | no | `0` | Inline this many of each result's most recent episodes (`episode_slug`, `title`, `published_at`, `duration_seconds`). `0` means none. | ## Output A markdown document with `## Podcast matches (N)` and one `### Title` section per podcast. Each section carries bulleted KV rows: `- **Slug:**`, `- **Episodes:**`, `- **Bias:**` (when rated; values: `EXTREME_LEFT`, `LEANS_LEFT`, `LEFT`, `CENTER`, `RIGHT`, `LEANS_RIGHT`, `EXTREME_RIGHT`), `- **Suitability tier:**` (when assessed; values: `SAFE`, `LIMITED`, `SENSITIVE`, `UNSAFE`), `- **Language:**` (only those with values), then a paragraph description, then a `Top speakers:` heading followed by up to 8 bullets — each formatted `Name (entity_slug) — role`. When `include` contains `suitability` and the podcast has been assessed, a `Suitability breakdown:` block follows with `Confidence`, `Episodes analyzed`, `Window`, the summary paragraph, and one bullet per category above `NONE`, formatted `**code** — prevalence=…, treatment=…, risk=…` with the reasoning and cited evidence excerpts. Categories at `NONE` are omitted (the dedicated REST endpoint [`GET /v1/podcasts/{id}/suitability`](/podcasts/suitability) returns all 12 unconditionally). When `include` contains `external_links`, an `External links:` block follows with one bullet per platform, formatted `Platform: ` (or `Platform: ` for handle-only platforms like `X (Twitter): @hardfork`). When the platform reports attributes (audience size, content tallies, verified status, ...) they are appended after an em dash, joined with `·` and formatted `name: value` — so a YouTube channel renders as `YouTube: — subscribers: 245,000 · videos: 1,200`. Integer counts carry thousand separators. When `include` contains `format` and the podcast's episodes have been analyzed, a `Format:` block follows with `Guest frequency` (bucket plus the exact share of analyzed episodes), `Signals`, `Ads`, `Video`, `Episode length` (average with p25/median/p75), `Cadence` (episodes per week), `Publishing` (active/dormant with the latest episode date), `Publish days` (per-weekday episode counts), and `Analyzed episodes` (the sample behind the attributes). Fields the catalog can't yet assert for the show are omitted rather than guessed. When `recent_episodes` is greater than 0, a `Recent episodes:` block follows with up to that many bullets per podcast, formatted `YYYY-MM-DD · episode-slug — Episode title (~Nm)`. The `episode-slug` is the handle accepted by [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode). The `ratings_summary`, `bias`, and `rankings` hydrations are carried in full on the JSON form of the response (pass `output_format: "json"` to receive it); the markdown rendering surfaces the high-level `Bias` / `Suitability tier` enums and the suitability/format/external-link/recent-episode blocks above. Sample (`slug="all-in"`): ```markdown theme={"dark"} ## Podcast matches (1) ### All-In with Chamath, Jason, Sacks & Friedberg - **Slug:** all-in - **Episodes:** 201 - **Bias:** LEANS_RIGHT - **Language:** en Industry veterans, degenerate gamblers & besties Chamath Palihapitiya, Jason Calacanis, David Sacks & David Friedberg cover all things economic, tech, political, social & poker. Top speakers: - Jason Calacanis (jason-calacanis) — HOST - Chamath Palihapitiya (chamath-palihapitiya) — HOST - David Friedberg (david-friedberg) — HOST - David O. Sacks (david-o-sacks) — HOST ``` The string after `- **Slug:**` is the podcast handle to feed back as `podcast_slug` to other tools. Each speaker bullet's parenthesized `(entity_slug)` (when present) is the knowledge-graph handle for that person — feed it into [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) or [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes) to follow them across the catalog. ## Example ```text theme={"dark"} Agent calls: particle_podcast_resolve { "query": "all in" } → reads "- **Slug:**" row → "all-in" Agent calls: particle_podcast_get_sponsors { "podcast_slug": "all-in" } ``` Pulling subscription URLs and current chart positions for a known podcast in one call: ```text theme={"dark"} Agent calls: particle_podcast_resolve { "slug": "all-in", "include": ["external_links", "rankings"] } ``` Looking up a podcast by its Apple feed: ```text theme={"dark"} Agent calls: particle_podcast_resolve { "itunes_id": "1502871393" } ``` ## Related * REST equivalent: [`GET /v1/podcasts`](/podcasts/overview). * For sponsor analytics on a single podcast, use [`particle_podcast_get_sponsors`](/mcp/tools/podcast_advertising/podcast-get-sponsors). * For chart movers and history, use [`particle_podcast_get_rankings`](/mcp/tools/podcast_rankings/podcast-get-rankings); for the full review list, [`particle_podcast_get_ratings`](/mcp/tools/podcast_ratings/podcast-get-ratings). # particle_podcast_search_transcripts Source: https://docs.particle.pro/mcp/tools/podcasts/podcast-search-transcripts Semantic, keyword, or hybrid search over podcast transcripts — the single way to retrieve relevant dialogue, segments, and clips. Search the podcast catalog by what is said in episodes — by meaning (`semantic_search`), by exact phrase (`keyword_search`), or both at once (hybrid ranking). This is **THE** way to retrieve relevant dialogue, segments, and clips: each result is one segment of one episode with bounded transcript windows pinpointing the highest-relevance lines, plus any highlight clips that overlap the segment, inline on the match. This tool replaces the old `search_dialogue` and `list_clips` tools. There is no separate clip-search tool — relevant clips arrive on these matches. A known episode's full clip list is [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) with `include: ["clips"]`. ## Segments vs clips * **Segments tile an episode end-to-end** — every minute belongs to exactly one segment. Search ranks segments and returns the matching ones. * **Clips are sparse, engagement-ranked highlights** that overlap some segments. When a clip overlaps a ranked segment, it appears under an `Overlapping clips:` line on that match. Use this for "find dialogue *about* a topic". For "every line *naming* a person or company" use [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) instead — `person_slug` and `company_slug` here narrow ranked results, they don't drive the ranking. ## Inputs At least one of `semantic_search` or `keyword_search` is required. | Field | Type | Required | Default | Description | | ----------------- | -------------- | -------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `semantic_search` | string | one of | — | Vector-similarity search by meaning. Express the query the way you'd describe the topic to a colleague — paraphrase tolerant. Combine with `keyword_search` for hybrid ranking. | | `keyword_search` | string | one of | — | BM25 lexical search over dialogue. Use when an exact token must appear (tickers, drug names, model numbers). | | `person_slug` | string | no | — | Person slug from `particle_person_resolve` or `particle_entity_resolve` (e.g. `"sam-altman"`). Filters results to episodes featuring this person. Also accepts a bare entity slug for non-person entities like places. For "every line about X" use `particle_podcast_find_mentions` instead. | | `company_slug` | string | no | — | Company slug, domain, or ID. Resolves to the company's linked entity and applies as a filter. | | `podcast_slug` | string | no | — | Podcast slug, internal ID, or numeric iTunes ID. | | `episode_slug` | string | no | — | Filter to a specific episode by slug or ID. | | `segment_type` | string | no | — | Segment type filter. One of: `INTRO`, `PERSONAL_BANTER`, `TOPIC_DISCUSSION`, `INTERVIEW`, `TRANSITION`, `AD`, `OUTRO`. | | `role` | string | no | — | Speaker/mention role filter on the `person_slug` or `company_slug` filter. One of: `guest`, `host`, `panelist`, `correspondent`, `mention`. | | `since` | string | no | — | Only segments from episodes published on or after this ISO 8601 date. | | `until` | string | no | — | Only segments from episodes published on or before this ISO 8601 date. | | `sort` | string | no | `relevance` | Sort order. One of: `relevance`, `recency`. | | `context` | integer (1–15) | no | 1 | Lines of surrounding dialogue around each matched line. Widens each match window **in place** — raise it instead of fetching the full transcript when a match needs more context. | | `limit` | integer (1–50) | no | 10 | Results per page. | | `cursor` | string | no | — | Opaque pagination cursor. | ## Output A markdown document with `## Dialogue matches (N)` and one `### Segment title` section per match. Each section carries bulleted KV rows: `- **Episode:**`, `- **Podcast:**`, `- **Podcast slug:**`, `- **Published:**`, `- **Episode slug:**`, `- **Segment type:**`, `- **Match:**` (the source: `semantic`, `keyword`, or `hybrid`), then one or more bullets formatted `Window @ start–end` (or `Preview @ …` for truncated windows) followed by indented dialogue lines. Every line that matched the query is rendered with the entire `Speaker: text` wrapped in bold (a window can contain several matched lines); surrounding context renders as `**Speaker:** text`. When highlight clips overlap the segment, they appear under an `Overlapping clips:` line as bullets formatted `Title (TYPE, score N) @ start–end — clip ID: `. When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended. Bolded lines mark the lines that ranked for the current query (a window can contain several). This is a different signal from the bolding in [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions), where a bolded line means "this line names the resolved entity" — they answer different questions and can both be true within the same episode for different reasons. Sample (`semantic_search="how AI affects the labor market", limit=2`): ```markdown theme={"dark"} ## Dialogue matches (2) ### Labor Market Transformation from AI - **Episode:** Where Investment Themes Intersect and Beat Markets - **Podcast:** Thoughts on the Market - **Podcast slug:** thoughts-on-the-market - **Published:** 2026-04-20 - **Episode slug:** where-investment-themes-intersect-and-beat-markets - **Segment type:** TOPIC_DISCUSSION - **Match:** semantic - Window @ 108s–143s **Stephen Byrd: Now, at the same time, AI is reshaping the labor market.** **Stephen Byrd:** And on net, we see a four percent job loss driven by eleven percent of outright elimination… ### Goldman Sachs Report on AI and Job Displacement - **Episode:** TNB Tech Minute: Thrive Capital and Andreesen Horowitz Co-lead … - **Podcast:** WSJ Tech News Briefing - **Podcast slug:** wsj-tech-news-briefing - **Published:** 2026-03-03 - **Episode slug:** tnb-tech-minute-thrive-capital-and-andreesen-horowitz-co-lead-multibillion - **Segment type:** TOPIC_DISCUSSION - **Match:** semantic - Window @ 43s–67s **Danny Lewis: Speaking of AI, a new research report by Goldman Sachs is predicting the technology will eventually displace eleven million jobs…** Overlapping clips: - AI to displace 11 million jobs (SHOCKING, score 84) @ 40s–72s — clip ID: e8342676-… --- **Cursor:** r.4gfFC6 ``` The string after `- **Episode slug:**` is what you can feed into [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) for the full transcript or clip list, or into [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions) (`format=detail`, `episode_slug`) for mention windows around a specific person. ## Example ```text theme={"dark"} Agent calls: particle_podcast_search_transcripts { "semantic_search": "how AI affects the labor market", "podcast_slug": "all-in", "since": "2024-01-01", "context": 4, "limit": 5 } ``` For hybrid ranking, populate both `semantic_search` and `keyword_search`: ```text theme={"dark"} Agent calls: particle_podcast_search_transcripts { "semantic_search": "what investors think of GPT-5", "keyword_search": "GPT-5", "limit": 10 } ``` ## Related * REST equivalent: [`GET /v1/podcasts/episodes/search`](/podcasts/search). * For "every line naming X" use [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions). * For a known episode's full clip list, use [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) with `include: ["clips"]`. * For metadata-only episode discovery (no dialogue), use [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes). # particle_call Source: https://docs.particle.pro/mcp/tools/system/call Dispatch any public Particle tool by name — a compatibility fallback for harnesses that block un-advertised tools. Dispatch any public Particle tool by name. This is a **compatibility fallback** for harnesses that block calling tools that weren't advertised on `tools/list` — every public Particle tool is executable by name, so prefer calling discovered tools directly when your harness allows it. It's always-on (the `system` category can never be excluded) and free to invoke (tier `free`, cost `0`). Identical metering and plan gating apply whether you call a tool directly or through `particle_call` — the dispatch path mirrors a direct call exactly (request log, metering, pricing gate, premium gate, execute), and the inner tool's `output_format` is honored. Use [`particle_catalog`](/mcp/tools/system/catalog) to discover tool names and input schemas. ## Inputs | Field | Type | Required | Default | Description | | ----------- | ------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `tool` | string | yes | — | Flat name of any public Particle tool (e.g. `"particle_podcast_get_episode"`). Discover names and schemas with `particle_catalog`. | | `arguments` | object | no | `{}` | Arguments object for the target tool, matching its input schema. | Unknown or internal tool names return an actionable error suggesting `particle_catalog`. The MCP surface's contract is that "all tools" means all **public** tools — internal tools are never dispatchable here even though the registry could run them. ## Output The target tool's own response, passed through unchanged — the same markdown text block the tool would return if called directly (or its JSON form when the inner `arguments` include `output_format: "json"`). A call through `particle_call` is indistinguishable from a direct one. ## Example ```text theme={"dark"} # Harness blocks un-advertised tool_use names; reach an opt-in tool via the fallback. Agent calls: particle_call { "tool": "particle_podcast_get_bias_leaderboard", "arguments": { "metric": "most_right_leaning", "limit": 10 } } → returns the same output as calling particle_podcast_get_bias_leaderboard directly ``` ## Related * [Tool sets & discovery](/mcp/tool-sets) — why every public tool is callable by name regardless of advertisement. * [`particle_catalog`](/mcp/tools/system/catalog) — discover tool names and input schemas first. # particle_catalog Source: https://docs.particle.pro/mcp/tools/system/catalog Browse the full Particle tool catalog — every exposure category, its tools, and (with a category) full input schemas. Browse the full Particle tool catalog. Your `tools/list` shows the [default categories](/mcp/tool-sets) plus the always-on `system` category (which is why `particle_catalog` itself always appears), but **every public Particle tool is callable by name** regardless of what was advertised — call this tool to discover the rest. It's always-on (the `system` category can never be excluded) and free (tier `free`, cost `0`). * **Without arguments:** the categorical menu — every category with its tool names, tiers, and one-line summaries. * **With `category`:** the full input schema for each of that category's tools, ready to call. The typical discovery loop: call `particle_catalog` with no arguments to see what exists, then `particle_catalog` with a `category` to get the input schemas, then call the tool directly — or via [`particle_call`](/mcp/tools/system/call) if your harness blocks un-advertised tool names. ## Inputs | Field | Type | Required | Default | Description | | ---------- | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `category` | string | no | — | Exposure category name (e.g. `"podcast_bias"`). When set, the response includes the FULL input schema for every tool in that category — call this before invoking a tool you haven't seen advertised. When omitted, returns the categorical menu: every category with its tools and one-line summaries. | An unknown `category` returns an actionable error suggesting a no-argument call to list every category. ## Output A markdown document with `## Particle tool catalog` and one `### (default|opt-in|always-on)` section per category. Each section carries the category summary and one bullet per tool, formatted `**tool_name** (tier) — summary`. In detail mode (a `category` was supplied), each tool bullet is followed by its input schema rendered as a fenced JSON block. Sample (no arguments, abridged): ```markdown theme={"dark"} ## Particle tool catalog ### system (always-on) Discovery meta-tools: browse the full tool catalog and call any tool by name. - **particle_call** (free) — Dispatch any public Particle tool by name - **particle_catalog** (free) — Browse the full Particle tool catalog ### podcasts (default) Resolve podcasts, list and fetch episodes, search transcripts, and find entity mentions. - **particle_podcast_resolve** (standard) — Find a podcast by free-text title, exact slug, iTunes ID, or RSS feed URL - **particle_podcast_search_transcripts** (premium) — Search the podcast catalog by what is said in episodes - … ### podcast_bias (opt-in) Corpus-wide political-bias views: publisher leaderboards and publishers by bias result. - **particle_podcast_get_bias_leaderboard** (premium) — Rank podcast publishers by political-bias metrics across their analyzed catalogs - **particle_podcast_list_bias_publishers** (premium) — Which publishers have the most podcasts in one political-bias bucket ``` ## Example ```text theme={"dark"} # Discover what exists, then drill into a category for schemas. Agent calls: particle_catalog {} → reads the category menu, spots "podcast_bias (opt-in)" Agent calls: particle_catalog { "category": "podcast_bias" } → reads the full input schema for particle_podcast_get_bias_leaderboard Agent calls: particle_podcast_get_bias_leaderboard { "metric": "most_right_leaning" } (directly — or via particle_call if the harness blocks it) ``` ## Related * [Tool sets & discovery](/mcp/tool-sets) — the full selector mechanics and the always-callable contract. * [`particle_call`](/mcp/tools/system/call) — dispatch any discovered tool by name. # particle_topic_browse Source: https://docs.particle.pro/mcp/tools/topics/topic-browse Navigate the topic taxonomy — without `parent_slug` returns top-level roots; with it returns direct children. Navigate the topic taxonomy. Without `parent_slug`, returns the top-level roots (Politics, Business, Technology, etc.). With `parent_slug` set, returns the direct children of that topic. Topic slugs use a `parent/child` convention (e.g. `politics/elections`) and let agents browse the hierarchy to find well-named categories. The slugs round-trip into the `topic_slug` filter on [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) and [`particle_podcast_list_guests`](/mcp/tools/podcast_guests/podcast-list-guests), and appear on episodes via [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) with `include: ["topics"]`. ## Inputs | Field | Type | Required | Default | Description | | ------------- | --------------- | -------- | ------- | -------------------------------------------------------------------------------------- | | `parent_slug` | string | no | — | Topic slug or ID. Returns the direct children of this topic. Omit for top-level roots. | | `limit` | integer (1–100) | no | 50 | Topics per page. | | `cursor` | string | no | — | Opaque pagination cursor from a previous response. | ## Output A markdown document with an `## Topics (N)` heading and a flat bullet list — one bullet per topic, formatted `**Name** — slug: ` (with ` — ` appended when an ancestry path is available). When more pages exist, a horizontal rule and a `**Cursor:** ` line are appended; pass that cursor value back as the `cursor` input. Sample (`limit=5`): ```markdown theme={"dark"} ## Topics (5) - **Society** — slug: society - **Government** — slug: government - **Business** — slug: business - **Politics** — slug: politics - **Media** — slug: media --- **Cursor:** r.4gfFC9 ``` The string after `slug:` is what you pass back as `parent_slug` to drill into a topic's children. ## Example ```text theme={"dark"} Agent calls: particle_topic_browse {} → reads "slug: politics" → "politics" Agent calls: particle_topic_browse { "parent_slug": "politics" } → returns child topics like "politics/elections" ``` ## Related * REST equivalent: [`GET /v1/topics`](/knowledge-graph/topics). * Feed a topic slug into [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve) (`topic_slug`) or [`particle_podcast_list_guests`](/mcp/tools/podcast_guests/podcast-list-guests) (`topic_slug`). # Advertising Source: https://docs.particle.pro/podcasts/advertising Sponsor analytics across the podcast catalog: leaderboards, per-company presence, and co-occurrence. Particle API extracts every sponsor read from every transcribed episode, attributes them to a company in the [knowledge graph](/knowledge-graph/entities), and aggregates the result. You get sponsor-side analytics — who advertises where, how often, with what read style — without scraping audio yourself. Available to MCP agents in the opt-in `podcast_advertising` category as [`particle_company_get_podcast_ad_presence`](/mcp/tools/podcast_advertising/company-get-podcast-ad-presence), [`particle_podcast_get_sponsors`](/mcp/tools/podcast_advertising/podcast-get-sponsors), and [`particle_podcast_get_sponsor_leaderboard`](/mcp/tools/podcast_advertising/podcast-get-sponsor-leaderboard). ## Per-company advertising The fastest way to see how a single company shows up as a sponsor: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/companies/nvidia/podcast/advertising" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/companies/nvidia/podcast/advertising", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const summary = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/companies/nvidia/podcast/advertising", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) summary = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "company_id": "nvidia.com", "total_ads": 13, "podcast_reach": 3, "episode_reach": 10, "read_type_breakdown": { "host_read": 12, "pre_recorded": 1 }, "recent_ads": [ { "sponsor_name": "NVIDIA Studio", "read_type": "HOST_READ", "placement_type": "PRE_ROLL", "start_seconds": 14.2, "end_seconds": 78.6, "duration_seconds": 64.4, "podcast": { "title": "Giant Bombcast", "slug": "giant-bombcast" }, "created_at": "2026-03-26T02:22:26Z" }, { "sponsor_name": "NVIDIA", "product": "Arc Raiders", "read_type": "HOST_READ", "placement_type": "PRE_ROLL", "podcast": { "title": "Kinda Funny Gamescast", "slug": "kinda-funny-gamescast" } } // … ] } ``` `read_type_breakdown` distinguishes host-reads from pre-recorded spots. `placement_type` distinguishes `PRE_ROLL` / `MID_ROLL` / `POST_ROLL`. `recent_ads` includes per-product attribution where the read called out a specific product line, plus segment timing within the episode — see [Per-episode ads](#per-episode-ads) below for details. ## Per-podcast advertising summary Same idea, inverted — for a single podcast, who's sponsoring it? ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/all-in/advertising" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "total_ads": 47, "top_sponsors": [ { "sponsor_name": "OKX", "company_id": "okx.com", "ad_count": 7, "episode_count": 7 }, { "sponsor_name": "Circle", "company_id": "circle.com", "ad_count": 5, "episode_count": 5 }, { "sponsor_name": "Solana", "company_id": "solanalabs.com", "ad_count": 4, "episode_count": 4 }, { "sponsor_name": "Polymarket", "company_id": "polymarket.com", "ad_count": 3, "episode_count": 3 } // … ] } ``` A useful proxy for category fit — crypto-native sponsors dominate All-In, for example. ## Per-episode ads When you already have an episode in hand — for example, drilling in after `list-episodes` or after a transcript-based search — fetch the individual ad reads detected in that episode: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/ads" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/ads", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/ads", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "5n8KqRrPmTyVxWzAbCdEf2", "sponsor_name": "OKX", "product": "OKX Wallet", "offer_description": "Sign up at okx.com/allin", "sponsor_url": "https://www.okx.com/allin", "read_type": "HOST_READ", "placement_type": "MID_ROLL", "start_seconds": 2110.3, "end_seconds": 2246.5, "duration_seconds": 136.2, "company": { "id": "okx.com", "name": "OKX" }, "podcast": { "title": "All-In with Chamath, Jason, Sacks & Friedberg", "slug": "all-in" }, "created_at": "2026-04-12T14:08:31Z" }, { "sponsor_name": "Polymarket", "read_type": "HOST_READ", "placement_type": "PRE_ROLL", "start_seconds": 12.0, "end_seconds": 47.8, "duration_seconds": 35.8, "company": { "id": "polymarket.com", "name": "Polymarket" }, "podcast": { "title": "All-In with Chamath, Jason, Sacks & Friedberg", "slug": "all-in" } } // … ], "has_more": false } ``` `company` resolves the sponsor read to a knowledge-graph company so you can join against [company endpoints](/companies/overview). Network promos (cross-promotion of other shows in the same network) are filtered out. For rolled-up views across episodes, see [Per-podcast advertising summary](#per-podcast-advertising-summary) and the [Sponsor leaderboard](#sponsor-leaderboard) below. Each ad carries the segment timing — `start_seconds`, `end_seconds`, `duration_seconds` — measured against the episode timeline. That's enough to align ads with the transcript, measure read length, or join with the raw audio offset for downstream analysis. ## Sponsor leaderboard The most-active sponsors across the catalog: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/advertising/leaderboard?limit=5" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "rank": 1, "sponsor": { "name": "Grainger", "company": { "id": "grainger.com", "name": "W. W. Grainger" } }, "ad_count": 14055, "podcast_reach": 538, "episode_reach": 9621 }, { "rank": 2, "sponsor": { "name": "Mint Mobile", "company": { "id": "mintmobile.com", "name": "Mint Mobile" } }, "ad_count": 9935, "podcast_reach": 610, "episode_reach": 8809 }, { "rank": 3, "sponsor": { "name": "Shopify", "company": { "id": "shopify.com", "name": "Shopify" } }, "ad_count": 9288, "podcast_reach": 576, "episode_reach": 8237 }, { "rank": 4, "sponsor": { "name": "Progressive", "company": { "id": "progressive.com", "name": "Progressive Corporation" } }, "ad_count": 8216, "podcast_reach": 607, "episode_reach": 5604 }, { "rank": 5, "sponsor": { "name": "FanDuel", "company": { "id": "fanduel.com", "name": "FanDuel" } }, "ad_count": 7298, "podcast_reach": 279, "episode_reach": 5394 } // … ] } ``` Filter by `metric` (`ad_count`, `podcast_reach`, or `episode_reach`) to change the ranking dimension; by `since` / `until` to scope to a time window; or by `company_id` to constrain results to one sponsor's brand family. ## Sponsor co-occurrence Which sponsors run alongside each other on the same episodes — a useful signal for media-buying overlap, attribution analysis, or competitive positioning: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/advertising/co-occurrence?limit=5" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "sponsor_a": { "name": "Indeed", "company": { "id": "indeed.com", "name": "Indeed" } }, "sponsor_b": { "name": "FanDuel", "company": { "id": "fanduel.com", "name": "FanDuel" } }, "shared_episodes": 1172 }, { "sponsor_a": { "name": "Shopify", "company": { "id": "shopify.com", "name": "Shopify" } }, "sponsor_b": { "name": "Mint Mobile", "company": { "id": "mintmobile.com", "name": "Mint Mobile" } }, "shared_episodes": 1072 } // … ] } ``` ## Sponsor lookup Resolve a sponsor by domain, slug, or canonical ID, then list the podcasts they sponsor: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/advertising/sponsors/grainger.com" \ -H "X-API-Key: $PARTICLE_API_KEY" curl "https://api.particle.pro/v1/podcasts/advertising/sponsors/grainger.com/podcasts?limit=10" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Related * [Companies → Overview](/companies/overview) — resolve any company by ticker, domain, or CIK before pulling its ad presence * [Concepts → Pricing weight](/concepts#pricing-weight) — advertising endpoints are priced higher per call * [Knowledge graph → Entities](/knowledge-graph/entities) — sponsors map to entities for cross-content tracking # Episodes Source: https://docs.particle.pro/podcasts/episodes Discover and filter episodes across the catalog, with rich per-episode sub-resources. The cross-podcast episode endpoint is the primary discovery tool. Filter by podcast, entity, company, language, date range, or duration. Every episode comes with sub-resources for transcripts, segments, clips, speakers, entities, and topics. Tracking *new* episodes as they're ingested? Don't poll this list with `published_after` — use the [episode feed](/podcasts/feed), a resumable poll that returns each new episode exactly once, or the real-time [episode stream](/podcasts/stream) (Enterprise). Available to MCP agents as [`particle_podcast_list_episodes`](/mcp/tools/podcasts/podcast-list-episodes) and (per-episode) [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode). ## List episodes ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes?entity_id=sam-altman&limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const url = new URL("https://api.particle.pro/v1/podcasts/episodes"); url.searchParams.set("entity_id", "sam-altman"); url.searchParams.set("limit", "3"); const res = await fetch(url, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/episodes", params={"entity_id": "sam-altman", "limit": 3}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "78cgekLUjCJBUZbj3s5K8Y", "title": "WHCD Shooting Aftermath, Musk and Altman Face-Off, Spirit Airlines Bailout", "podcast": { "id": "QpMz7GYKfSNuUa6zKXA4Q", "title": "Pivot" }, "published_at": "2026-04-28T10:00:00Z", "duration_seconds": 4206, "has_transcript": true, "segment_count": 21, "clip_count": 8, "entity_count": 146, "speakers": [ /* … */ ] } // … ], "has_more": true, "cursor": "…" } ``` The `has_transcript`, `segment_count`, `clip_count`, and `entity_count` fields tell you what enrichment data is available before you fetch any sub-resource. ## Filter parameters | Parameter | Description | Example | | ------------------------------- | ------------------------------------------------------- | ---------------------------------------- | | `podcast_id` | Slug, ID, or numeric iTunes ID; restrict to one podcast | `?podcast_id=pivot` | | `entity_id` | Slug or ID; episodes featuring or mentioning the entity | `?entity_id=sam-altman` | | `company_id` | Slug, domain, or ID; episodes featuring a company | `?company_id=nvidia` | | `language` | ISO 639-1 code | `?language=en` | | `published_after` | ISO 8601 timestamp | `?published_after=2026-04-01T00:00:00Z` | | `published_before` | ISO 8601 timestamp | `?published_before=2026-04-30T00:00:00Z` | | `has_transcript` | Limit to transcribed episodes | `?has_transcript=true` | | `min_duration` / `max_duration` | Bounds in seconds | `?min_duration=1800&max_duration=7200` | Combine freely — for long-form transcribed Sam Altman episodes from this month, for example: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes?\ entity_id=sam-altman&\ has_transcript=true&\ min_duration=1800&\ published_after=2026-04-01T00:00:00Z&\ limit=10" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Get a single episode ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` The full Episode response is documented in the [API reference](/api-reference/introduction). ## Episodes for a specific podcast A convenience shape of `list-episodes` filtered by podcast: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/pivot/episodes?limit=10" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Sub-resources Every episode exposes its enrichment data as sub-resources: | Endpoint | Returns | | ------------------------------------------- | ---------------------------------------------------- | | `GET .../episodes/{id}/transcript` | Diarized dialogue (full transcript) | | `GET .../episodes/{id}/transcript/words` | Word-level timestamps | | `GET .../episodes/{id}/transcript/mentions` | Lines around mentions of an entity | | `GET .../episodes/{id}/segments` | Structural sections (intros, ads, topic discussions) | | `GET .../episodes/{id}/clips` | Highlight clips | | `GET .../episodes/{id}/speakers` | Identified speakers with speaking duration | | `GET .../episodes/{id}/entities` | Mentioned entities ranked by salience | | `GET .../episodes/{id}/topics` | Topic classifications | ## Speakers ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/speakers" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "name": "Kara Swisher", "role": "HOST", "speaking_duration_seconds": 1955.12, "entity_slug": "kara-swisher" }, { "name": "Scott Galloway", "role": "HOST", "speaking_duration_seconds": 1986.09, "entity_slug": "scott-galloway" }, { "name": "Elie Honig", "role": "SOUNDBITE_SPEAKER", "speaking_duration_seconds": 50.30, "entity_slug": "elie-honig" } // … ] } ``` Roles include `HOST`, `CO_HOST`, `GUEST`, `PANELIST`, `CORRESPONDENT`, `SOUNDBITE_SPEAKER`, and `ADVERTISER`. Filter by role on your side if you only want hosts and guests. The `entity_slug` (when present) bridges to the [knowledge graph](/knowledge-graph/entities). ## Entities Returns people, organizations, places, products, and concepts mentioned in the episode, ranked by **salience** — a relative relevance score for that episode where higher values indicate the entity is more central to the discussion. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/entities" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "entity": { "id": "1GmOP1C2UZtpnHKPoc", "slug": "openai", "name": "OpenAI" }, "salience": 0.0061, "occurrences": 11 }, { "entity": { "id": "1GmOP1C3KLh", "slug": "anthropic", "name": "Anthropic" }, "salience": 0.00024, "occurrences": 10 }, { "entity": { "id": "17PzxG1t12xzno", "slug": "sam-altman", "name": "Sam Altman" }, "salience": 0.00016, "occurrences": 5 } // … ] } ``` For the actual dialogue around mentions, see [transcripts → mentions](/podcasts/transcripts#transcript-mentions). ## Topics ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/topics" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Topics are drawn from the hierarchical [topics taxonomy](/knowledge-graph/topics) — useful for routing episodes to category-specific feeds. ## Related * [Transcripts](/podcasts/transcripts) — dialogue, words, mentions, SRT * [Segments & clips](/podcasts/segments-and-clips) — structural breakdown and highlights * [Knowledge graph → entities](/knowledge-graph/entities) — follow speakers and topics across the catalog # External links Source: https://docs.particle.pro/podcasts/external-links Every third-party platform on which a podcast has a presence — directories, social profiles, video channels, websites — with resolved URLs and per-platform metadata. A podcast lives in many places: it's on Apple Podcasts and Spotify, the host posts to X, the publisher runs a YouTube channel, and there's a marketing site on its own domain. The **external-links** endpoint returns every one of those presences in a single, normalized shape — with the platform-native identifier, a ready-to-use web URL, and a list of optional per-platform attributes (subscribers, followers, handle, etc.). This page is the **forward** index: given a Particle podcast, list every third-party identifier we have for it. To go the other way — given an Apple, Spotify, or YouTube identifier, find the matching Particle podcast (in bulk, deterministically) — use [`GET /v1/podcasts/lookup`](/podcasts/lookup). Use this endpoint when you want to: * Build a "follow this podcast on…" UI without hard-coding per-platform URL templates. * Compare audience size across platforms (Spotify followers vs. YouTube subscribers vs. Castbox subscribers). * Pull the publisher's website and social handles for outreach or research. * Reconcile a podcast across third-party catalogs (Apple ID ↔ Spotify show ID ↔ Podchaser ID). *If you start with the third-party identifier instead, use [`GET /v1/podcasts/lookup`](/podcasts/lookup) — it's the reverse direction of this same index.* ## List external links for a podcast ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/hard-fork/external-links" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/hard-fork/external-links", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/hard-fork/external-links", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` The path parameter accepts a slug (`hard-fork`, `all-in`), the internal podcast ID, or a numeric Apple/iTunes collection ID — same as every other podcast sub-resource. ```jsonc Response (truncated) theme={"dark"} { "data": [ { "platform": { "name": "apple", "display_name": "Apple Podcasts", "type": "podcast_directory" }, "identifier": "1528594034", "url": "https://podcasts.apple.com/podcast/id1528594034" }, { "platform": { "name": "spotify", "display_name": "Spotify", "type": "music_service" }, "identifier": "44fllCS2FTFr2x2kjP9xeT", "url": "https://open.spotify.com/show/44fllCS2FTFr2x2kjP9xeT", "attributes": [ { "name": "followers", "category": "audience_size", "value": 3902, "observed_at": "2026-04-22T11:00:00Z" } ] }, { "platform": { "name": "youtube", "display_name": "YouTube", "type": "video_channel" }, "identifier": "UCZcR2SVWaGWNlMqPxvQS3vw", "url": "https://www.youtube.com/channel/UCZcR2SVWaGWNlMqPxvQS3vw", "attributes": [ { "name": "handle", "category": "profile", "value": "hardfork" }, { "name": "subscribers", "category": "audience_size", "value": 245000 }, { "name": "videos", "category": "content", "value": 200 }, { "name": "views", "category": "audience_size", "value": 12345678 } ] }, { "platform": { "name": "tiktok", "display_name": "TikTok", "type": "social_profile" }, "identifier": "hardfork", "url": "https://www.tiktok.com/@hardfork" }, { "platform": { "name": "twitter", "display_name": "X (Twitter)", "type": "social_profile" }, "identifier": "HardFork", "url": "https://twitter.com/HardFork" }, { "platform": { "name": "website", "display_name": "Website", "type": "website" }, "url": "https://www.nytimes.com/column/hard-fork" } // … ], "has_more": false } ``` Per-podcast cardinality is bounded — at most one entry per platform — so the endpoint returns the full set in a single response by default. Pass `?limit=` and `?cursor=` if you specifically want to page through results. ## Anatomy of an entry Every entry has the same shape regardless of platform. Optional fields are omitted when no data is available. | Field | Description | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `platform.name` | Stable machine name (e.g. `spotify`, `youtube`). Suitable for filtering, routing, or persistence. | | `platform.display_name` | Human-readable name suitable for UI display (`Apple Podcasts`, `X (Twitter)`). | | `platform.type` | Broad category. Use this to group presences in a UI rather than enumerating every name. | | `identifier` | Platform-native value (numeric id, slug, handle, or URL). Omitted entirely for the `website` platform — the JSON key is absent, not an empty string. | | `url` | Canonical web URL for this presence. Built from a per-platform template, or passed through for `website`. | | `attributes` | Optional per-platform metadata (audience size, profile fields, etc.). May be absent or empty. | ### Platform types `platform.type` lets clients render related platforms with a shared treatment without knowing the full list of names: | Type | What it covers | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `podcast_directory` | Apple Podcasts, Spotify-as-podcast-directory replicas, Castbox, Podchaser, Player FM, Podcast Addict, Podbean, Podcast Republic, Goodpods, The Podcast App, TuneIn, iHeartRadio. | | `music_service` | Spotify, Apple Music–adjacent stores, Audible, Amazon Music — services whose primary brand is audio playback. | | `social_profile` | X (Twitter), Instagram, Threads, TikTok, Facebook, LinkedIn — handle-based identity. | | `video_channel` | YouTube channel for the podcast (and any future video-first platforms). | | `community` | Reddit, Discord, Patreon — where listeners gather around the show. | | `website` | The publisher's own marketing site for the podcast. The `identifier` is omitted; `url` carries the destination. | | `other` | Reserved for future platforms that don't fit the above. Treat as opaque. | ### Attributes Each entry's `attributes` is a list of typed records. The set of attribute names that may appear varies per platform — and grows over time as upstream sources expose richer metadata. Clients should treat unknown attribute names additively rather than enumerating them. | Field | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Platform-native attribute name (`subscribers`, `followers`, `handle`, etc.). | | `category` | Broad grouping. Use this to render unfamiliar attributes without recognising the exact name. | | `value` | The attribute value. JSON type matches the category — numbers for audience/content tallies, strings for profile/account fields, booleans for flags. | | `observed_at` | ISO 8601 timestamp of when the attribute was last observed from the upstream source. Omitted when unavailable. | #### Attribute categories | Category | Typical names | Value type | | ---------------- | ---------------------------------------------------- | -------------------------------------------- | | `audience_size` | `subscribers`, `followers`, `views` | integer | | `content` | `videos` | integer | | `profile` | `handle`, `display_name`, `thumbnail_url`, `founded` | string (or RFC 3339 timestamp for `founded`) | | `account_status` | `verified` (forthcoming) | boolean | ## Platform reference The full list of platforms surfaced today, with the URL templates the API uses to build the `url` field. New platforms may be added without notice; existing platform names are stable. | `name` | `display_name` | `type` | URL pattern | | -------------- | ---------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `amazon_music` | Amazon Music | `music_service` | `https://music.amazon.com/podcasts/{id}` | | `apple` | Apple Podcasts | `podcast_directory` | `https://podcasts.apple.com/podcast/id{id}` | | `audible` | Audible | `music_service` | `https://www.audible.com/pd/{id}` | | `castbox` | Castbox | `podcast_directory` | `https://castbox.fm/channel/id{id}` | | `discord` | Discord | `community` | `https://discord.gg/{id}` | | `facebook` | Facebook | `social_profile` | `https://www.facebook.com/{id}` | | `goodpods` | Goodpods | `podcast_directory` | `https://www.goodpods.com/podcasts/{id}` | | `iheartradio` | iHeartRadio | `podcast_directory` | `https://www.iheart.com/podcast/{id}/` | | `instagram` | Instagram | `social_profile` | `https://www.instagram.com/{id}/` | | `itunes` | iTunes | `podcast_directory` | `https://podcasts.apple.com/podcast/id{id}` | | `linkedin` | LinkedIn | `social_profile` | `https://www.linkedin.com/company/{id}/` | | `patreon` | Patreon | `community` | `https://www.patreon.com/{id}` | | `playerfm` | Player FM | `podcast_directory` | `https://player.fm/series/{id}` | | `podaddict` | Podcast Addict | `podcast_directory` | `https://podcastaddict.com/podcast/{id}` | | `podbean` | Podbean | `podcast_directory` | `https://www.podbean.com/podcast-detail/{id}` | | `podchaser` | Podchaser | `podcast_directory` | `https://www.podchaser.com/podcasts/{id}` | | `podrepublic` | Podcast Republic | `podcast_directory` | `https://www.podcastrepublic.net/podcast/{id}` | | `reddit` | Reddit | `community` | `https://www.reddit.com/r/{id}/` | | `spotify` | Spotify | `music_service` | `https://open.spotify.com/show/{id}` | | `threads` | Threads | `social_profile` | `https://www.threads.net/@{id}` | | `tiktok` | TikTok | `social_profile` | `https://www.tiktok.com/@{id}` | | `tpa` | The Podcast App | `podcast_directory` | `https://thepodcastapp.com/p/{id}` | | `tunein` | TuneIn | `podcast_directory` | `https://tunein.com/podcasts/{id}/` | | `twitter` | X (Twitter) | `social_profile` | `https://twitter.com/{id}` | | `website` | Website | `website` | URL only; no separate `identifier` field | | `youtube` | YouTube | `video_channel` | `https://www.youtube.com/channel/{id}` for channel-ID identifiers (`UC` + 22 chars), `https://www.youtube.com/@{id}` for handle identifiers | ## YouTube: richer attributes YouTube is currently the most heavily-enriched platform. A YouTube entry can carry up to four attributes today: ```jsonc theme={"dark"} { "platform": { "name": "youtube", "display_name": "YouTube", "type": "video_channel" }, "identifier": "UCZcR2SVWaGWNlMqPxvQS3vw", "url": "https://www.youtube.com/channel/UCZcR2SVWaGWNlMqPxvQS3vw", "attributes": [ { "name": "handle", "category": "profile", "value": "hardfork" }, { "name": "subscribers", "category": "audience_size", "value": 245000 }, { "name": "videos", "category": "content", "value": 200 }, { "name": "views", "category": "audience_size", "value": 12345678 } ] } ``` The `display_name`, `thumbnail_url`, and `founded` attributes may also appear when known. ## Notes and gotchas ### `apple` and `itunes` are usually duplicates Apple Podcasts uses a single numeric ID across both its modern and iTunes-era surfaces. Most podcasts therefore have **two** entries — `apple` and `itunes` — with the same `identifier` and the same resolved `url`. This faithfully reflects what upstream catalog sources report; clients that want a single Apple entry can de-duplicate by `(type, identifier)`. ### Attributes are forward-compatible Both `attributes[].name` and `attributes[].category` are open enumerations. New names (e.g. a future `engagement_rate` for Instagram) and new categories will be added without a versioning event. Always render unknown attributes additively — match on `category` for grouping when the name isn't recognized. ### `observed_at` is best-effort Some attributes don't have a known observation timestamp; the field is then omitted. When present, the timestamp reflects the last time the value was refreshed from the upstream source — not the last time the *podcast* itself was updated. ### Identifier formats vary The `identifier` is whatever the platform natively uses. Examples: | Platform | Sample identifier | Shape | | -------------------- | -------------------------- | ---------------------------------------------- | | `apple` / `itunes` | `1528594034` | Numeric Apple Podcasts ID | | `spotify` | `44fllCS2FTFr2x2kjP9xeT` | Spotify show ID | | `youtube` | `UCZcR2SVWaGWNlMqPxvQS3vw` | Channel ID (or short handle for some podcasts) | | `twitter` / `tiktok` | `HardFork` / `hardfork` | Account handle (no `@`) | | `podbean` | `dir2423160,nrwgx` | Podbean-internal pair (don't try to parse) | The `website` platform is the exception: there's no separate identifier — the entry omits `identifier` entirely and only carries `url`. If you need a clickable link, prefer the resolved `url` over building one yourself; URL templates change. ## Choosing the right endpoint | You want to… | Use this | | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Show a "find this podcast on …" list of platforms (you have a Particle podcast → get all its external IDs) | `external-links` | | **Reverse**: you have a third-party ID → resolve to a Particle podcast | [`GET /v1/podcasts/lookup`](/podcasts/lookup) | | Compare audience size (followers / subscribers) across platforms | `external-links` (filter by `attributes[].category=audience_size`) | | Get the podcast's own metadata (title, publisher, description, RSS feed, episode counts) | [`GET /v1/podcasts/{id}`](/podcasts/overview) | | List episodes for a podcast | [`GET /v1/podcasts/{id}/episodes`](/podcasts/episodes) | | Sponsor / advertising profile | [`GET /v1/podcasts/{id}/advertising`](/podcasts/advertising) | | Political bias analysis | [`GET /v1/podcasts/{id}/bias`](/podcasts/overview#bias-analysis) | ## Related * [Lookup by external ID](/podcasts/lookup) — the reverse direction of this index: third-party identifier → Particle podcast. * [Podcasts overview](/podcasts/overview) — full Podcast object and other sub-resources. * [Episodes](/podcasts/episodes) — drill from a podcast into its individual episodes. * [Knowledge graph → entities](/knowledge-graph/entities) — the canonical record for the podcast's hosts, guests, and topics. # Episode feed (polling) Source: https://docs.particle.pro/podcasts/feed Resumable, strictly-ordered polling for newly ingested episodes — the all-plans alternative to the real-time stream. 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**](/podcasts/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**](/podcasts/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](/podcasts/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](#cold-starts-existing-data-or-a-lost-cursor).) 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](#cold-starts-existing-data-or-a-lost-cursor) 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](/podcasts/stream#pick-a-milestone), but the feed caps the podcast set it covers (see below): | Parameter | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `milestone` | `discovered`, `transcribed` (default), `segmented`, or `fully_ingested`. | | `podcast_ids` | Slugs 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_threshold` | Number in `(0,1)`; covers the **most-popular** podcasts at/above this popularity percentile. | | `topic_ids` | One 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`. | | `cursor` | High-water mark from a prior response — the exact ingestion-log position to resume from. Omit (with no `since`) to start from now. | | `since` | First-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](#cold-starts-existing-data-or-a-lost-cursor). | | `limit` | Max episodes per page (1–100, default 25). | | `include` | Heavy relations to embed in each episode (comma-separated): `transcript`, `segments`, `clips`, or `all`. Omitted by default. See [Hydrate the payload](#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](/podcasts/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](/podcasts) 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` 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 | — | 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. ```bash curl theme={"dark"} 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..." } ``` ```python Python theme={"dark"} import time, httpx, os PODCAST_IDS = "acquired,all-in,no-priors,my-first-million,the-tim-ferriss-show" cursor = load_saved_cursor() # None on the very first run while True: params = {"milestone": "transcribed", "limit": 100, "podcast_ids": PODCAST_IDS} if cursor: params["cursor"] = cursor res = httpx.get( "https://api.particle.pro/v1/podcasts/episodes/feed", params=params, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ).json() for episode in res["data"]: handle_episode(episode) # idempotent cursor = res["cursor"] save_cursor(cursor) # advance only after processing if not res["has_more"]: time.sleep(30) # caught up — poll on an interval ``` 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](/podcasts/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](/podcasts/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. ```bash theme={"dark"} 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. ## Related * [Episodes](/podcasts/episodes) — browse and filter the catalog * [Episode stream](/podcasts/stream) — real-time SSE delivery (Enterprise) # Format Profile Source: https://docs.particle.pro/podcasts/format What kind of show each podcast is — guest frequency, detected formats (interview, panel, call-in, solo), ads, video, episode length, and publishing cadence — as filterable attributes. Two shows about the same topic can be completely different products: a daily two-minute news brief read by a single narrator, and a weekly 90-minute interview show with rotating guests and a heavy ad load. Particle API derives that distinction from what's actually in the audio — speaker roles, durations, publish timestamps, and video discovery — and exposes it as a compact `format` object on every podcast, with every attribute doubling as a catalog filter. ## The format object Every podcast response (list and detail) embeds the compact form: ```json Response from GET /v1/podcasts/pivot (truncated) theme={"dark"} { "id": "QpMz7GYKfSNuUa6zKXA4Q", "title": "Pivot", "slug": "pivot", "format": { "guest_frequency": "regular", "signals": ["interview"], "has_ads": true, "has_video": true, "avg_episode_minutes": 62, "episodes_per_week": 2.1, "publishing_status": "active" } } ``` | Field | Meaning | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `guest_frequency` | How often episodes feature a guest: `never`, `rare`, `occasional`, `regular`, or `always`. | | `signals` | Detected production formats: `interview`, `panel`, `call_in`, `solo_narrated`. A show can carry several — a panel show with call-in segments carries both. | | `has_ads` | Whether episodes carry advertiser reads. The full sponsor breakdown lives at [Advertising](/podcasts/advertising). | | `has_video` | Whether any episode has a discovered video version (e.g. a YouTube release). | | `avg_episode_minutes` | Average episode length in minutes. | | `episodes_per_week` | Publishing cadence over the trailing 90 days — `5.0` for a weekday daily, `0.5` for a fortnightly show. | | `publishing_status` | `active` (an episode within the last 90 days) or `dormant`. | Attributes are **asserted, never presumed**: `guest_frequency`, `signals`, and `has_ads` are derived from speaker analysis and are omitted until enough of the show's episodes have been analyzed to judge. An omitted field means *not yet known* — it never means "no". The `format` object itself is omitted for podcasts whose episodes haven't been ingested yet. ### Guest frequency buckets | Value | Share of analyzed episodes with a guest | | ------------ | --------------------------------------- | | `never` | 0% — the show does not host guests | | `rare` | up to 25% | | `occasional` | 25–50% | | `regular` | 50–90% | | `always` | above 90% — interview-driven | ### Format signals | Signal | What it indicates | | --------------- | -------------------------------------------------------------------------------------------------------- | | `interview` | Guests appear in at least half of episodes — conversation with outside voices is the show's core format. | | `panel` | A recurring panel of commentators (review roundtables, sports panels, actual-play shows). | | `call_in` | Listener callers are part of the format (advice shows, talk radio). | | `solo_narrated` | No meaningful guest, panel, or caller presence — narrated news briefs, audio essays, solo commentary. | ## Get the full profile The dedicated endpoint returns the exact rates and distributions behind the compact attributes: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/pivot/format" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch("https://api.particle.pro/v1/podcasts/pivot/format", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); const profile = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/pivot/format", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) profile = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "guest_frequency": "regular", "signals": ["interview"], "has_ads": true, "has_video": true, "avg_episode_minutes": 62, "episodes_per_week": 2.1, "publishing_status": "active", "analyzed_episodes": 164, "episode_count": 167, "guest_episode_rate": 0.57, "ad_episode_rate": 0.94, "call_in_episode_rate": 0.0, "panel_episode_rate": 0.02, "episode_minutes_p25": 54, "episode_minutes_median": 61, "episode_minutes_p75": 70, "publish_days": { "monday": 2, "tuesday": 81, "wednesday": 1, "thursday": 0, "friday": 80, "saturday": 2, "sunday": 1 }, "first_episode_published_at": "2024-06-04T09:00:00Z", "latest_episode_published_at": "2026-06-02T09:00:00Z", "computed_at": "2026-06-03T14:22:51Z" } ``` `analyzed_episodes` is the sample size behind every role-derived attribute — surface it when you need to qualify a claim. The `publish_days` histogram reads the schedule directly: this show ships Tuesdays and Fridays. ## Filter the catalog Every compact attribute has a matching query parameter on [`GET /v1/podcasts`](/podcasts/overview#get-a-podcast), and they compose with the existing `topic_id`, `language`, `suitability_tier`, and `popularity_threshold` filters. Find ad-free interview shows: ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts" \ --data-urlencode "guest_frequency=regular,always" \ --data-urlencode "has_ads=false" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Find 20–40 minute commute-length business shows that are still publishing: ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts" \ --data-urlencode "topic_id=business" \ --data-urlencode "min_avg_episode_minutes=20" \ --data-urlencode "max_avg_episode_minutes=40" \ --data-urlencode "publishing_status=active" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Find weekday-daily news briefs (solo-narrated, \~5+ episodes a week): ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts" \ --data-urlencode "format_signal=solo_narrated" \ --data-urlencode "min_episodes_per_week=4" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` | Parameter | Matches | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `guest_frequency` | Comma-separated list of buckets; a podcast matches any listed bucket. | | `format_signal` | Comma-separated list of signals; a podcast matches any listed signal. | | `has_ads` | `true` or `false`. `false` is an assertion of ad-free-ness and requires enough analyzed episodes — sparsely-analyzed shows are excluded rather than presumed clean. | | `has_video` | `true` or `false`. | | `min_avg_episode_minutes` / `max_avg_episode_minutes` | Bounds on average episode length. | | `min_episodes_per_week` / `max_episodes_per_week` | Bounds on publishing cadence. | | `publishing_status` | `active` or `dormant`. | Format filters only match podcasts whose profile supports the claim — podcasts without enough analyzed episodes are excluded from role-derived filters rather than guessed at. ## Limitations * **Sample-based.** Role-derived attributes (`guest_frequency`, `signals`, `has_ads`) require at least 5 analyzed episodes; below that they are omitted and the podcast won't match those filters. Check `analyzed_episodes` on the full profile for the sample behind any claim. * **Cadence needs observation time.** `episodes_per_week` measures the trailing 90 days and is omitted for shows tracked more briefly — a show's full back catalog isn't part of the measurement, so cadence reflects current behavior, not lifetime averages. * **Dormancy is time-derived.** `publishing_status` flips to `dormant` 90 days after the latest tracked episode; a seasonal show between drops will read as dormant until its next release. ## Related * [Guests](/podcasts/guests) — the people behind `guest_frequency`: lifetime guest profiles, appearances, and trends. * [Advertising](/podcasts/advertising) — the full sponsor breakdown behind `has_ads`. * [Episodes](/podcasts/episodes) — per-episode speakers, durations, and videos. * [Brand Suitability](/podcasts/suitability) — the other show-level assessment surfaced compactly on every podcast object. # Guests Source: https://docs.particle.pro/podcasts/guests Track the people who appear on podcasts — lifetime profiles, every appearance, the shows they join, who's doing the rounds, and brand-suitability exposure. A *guest* is a **Person** — Particle's first-class entity for an individual, served in full at `GET /v1/people/{id}` — identified in a non-host listing role on at least one episode. The guest endpoints are built around that Person identity: every guest carries the same canonical handle used everywhere else on the platform, so the speaker you find in a transcript, the person served by the People API, and the guest in this directory are the same record. Resolve a guest by **slug** (recommended — e.g. `brad-gerstner`) or by the encoded Person ID returned as `id`. A guest is someone who *appeared* on an episode in a listing role — distinct from a [mention](/podcasts/mentions), which is any line of dialogue where a person is named, whether they're in the room or not. Use Guests to follow who shows up; use Mentions to follow who gets talked about. ## What you can build * **Booking and PR research** — find a guest's complete appearance history, the shows they favor, and who else those shows book. * **Press-tour detection** — surface people making the rounds right now with [trends](#trending-guests), the signal behind a book launch or product unveil. * **Brand-safety screening** — read a guest's [suitability exposure](#brand-suitability-exposure) across the shows they join before a sponsorship or partnership. * **Talent graphs** — pivot from a podcast to its [guest roster](#guest-roster-for-a-podcast) and back out to each guest's wider footprint. ## Choosing the right endpoint | You want… | Use this | | ------------------------------------------------ | ------------------------------------------------------------------------- | | Browse or search the directory of podcast guests | [`GET /v1/podcasts/guests`](#the-guest-directory) | | A single guest's lifetime profile and stats | [`GET /v1/podcasts/guests/{id}`](#a-guest-profile) | | Every episode a guest appeared on | [`GET /v1/podcasts/guests/{id}/appearances`](#appearances) | | The distinct shows a guest has appeared on | [`GET /v1/podcasts/guests/{id}/podcasts`](#podcasts-a-guest-has-joined) | | Who has been a guest on a specific podcast | [`GET /v1/podcasts/{id}/guests`](#guest-roster-for-a-podcast) | | Guests currently doing the podcast rounds | [`GET /v1/podcasts/guests/trends`](#trending-guests) | | A guest's brand-suitability exposure | [`GET /v1/podcasts/guests/{id}/suitability`](#brand-suitability-exposure) | The `{id}` in every endpoint above is a **Person** reference (slug or encoded ID). The one exception is the roster endpoint, [`GET /v1/podcasts/{id}/guests`](#guest-roster-for-a-podcast), where `{id}` is a **podcast** reference (slug, Particle ID, or Apple/iTunes ID) — it's the inverse view, listing the guests *of* a show. ## The guest directory `GET /v1/podcasts/guests` returns a paginated directory of everyone who has guested on the catalog, ranked by lifetime appearances by default. ```bash curl theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/guests" \ --data-urlencode "q=gerstner" \ --data-urlencode "min_appearances=5" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const params = new URLSearchParams({ q: "gerstner", min_appearances: "5" }); const res = await fetch( `https://api.particle.pro/v1/podcasts/guests?${params}`, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} import os, requests res = requests.get( "https://api.particle.pro/v1/podcasts/guests", params={"q": "gerstner", "min_appearances": 5}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) guests = res.json()["data"] ``` ```json Response theme={"dark"} { "data": [ { "id": "5njEX8HDc7oNdM8kr9ItRs", "slug": "brad-gerstner", "name": "Brad Gerstner", "appearance_count": 35, "distinct_podcasts": 10, "first_appearance_at": "2020-11-04T10:06:21Z", "last_appearance_at": "2026-05-29T22:16:05Z" } ], "has_more": false } ``` Each row is a compact guest: the Person identity (`id`, `slug`, `name`, and `image_url` when a headshot is known) plus lifetime `appearance_count` and `distinct_podcasts`. Pass any `slug` or `id` straight into the [profile endpoint](#a-guest-profile) to expand it. ### Filters | Param | Notes | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `q` | Case-insensitive substring match on the guest's display name. | | `min_appearances` | Minimum lifetime appearance count. Default 1. | | `podcast_id` | Slug or ID. Restrict to guests who have appeared on this podcast. When set, `appearance_count` is scoped to that show. | | `topic_id` | Restrict to guests with at least one appearance on an episode under this topic. | | `appeared_since` | ISO 8601 date. Guests with an appearance on or after this date. | | `suitability_tier_max` | `SAFE`, `LIMITED`, `SENSITIVE`, `UNSAFE`. Restrict to guests whose appearances are on shows at or below this IAB Tech Lab tier. | | `sort` | `appearances` (default) or `recency`. | ## A guest profile `GET /v1/podcasts/guests/{id}` returns the lifetime profile: the full Person payload — the same one served at `/v1/people/{id}` — plus a podcast-guest activity block. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/guests/brad-gerstner" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "id": "5njEX8HDc7oNdM8kr9ItRs", "slug": "brad-gerstner", "name": "Brad Gerstner", "description": "Founder, Chairman, and CEO of Altimeter Capital; co-host of BG2Pod", "detailed_description": "Brad Gerstner is an American entrepreneur, investor, and hedge fund manager who founded Altimeter Capital…", "external_links": [ { "platform": { "name": "linkedin", "display_name": "LinkedIn", "type": "social_profile" }, "identifier": "in/bradgerstner", "url": "https://www.linkedin.com/in/bradgerstner/" }, { "platform": { "name": "wikipedia", "display_name": "Wikipedia", "type": "other" }, "url": "https://en.wikipedia.org/wiki/Brad_Gerstner" } ], "knowledge_graph_entity": { "id": "1GmOP1C2tsrQUoriBU", "slug": "brad-gerstner", "wikipedia_url": "https://en.wikipedia.org/wiki/Brad_Gerstner" }, "stats": { "appearance_count": 35, "distinct_podcasts": 10, "first_appearance_at": "2020-11-04T10:06:21Z", "last_appearance_at": "2026-05-29T22:16:05Z", "total_speaking_seconds": 28012.23, "appearances_by_bias": { "NOT_POLITICAL": 3, "CENTER": 4, "LEANS_LEFT": 2, "LEANS_RIGHT": 19, "RIGHT": 7 }, "appearances_by_suitability_tier": { "SAFE": 28, "LIMITED": 5, "SENSITIVE": 2 } }, "top_podcasts": [ { "podcast": { "id": "4t9PU1WkzOroEXfBl6ia7r", "title": "All-In with Chamath, Jason, Sacks & Friedberg", "slug": "all-in", "image_url": "https://cdn.particle.pro/url/media/a0a7b2fa-…" }, "appearance_count": 19, "first_appearance_at": "2020-11-04T10:06:21Z", "last_appearance_at": "2026-05-08T22:16:00Z", "suitability_tier": "SAFE", "bias": "LEANS_RIGHT" } ] } ``` The Person fields (`description`, `detailed_description`, `external_links`, and — when a company affiliation is known — `current_role` and `roles`) carry the guest's identity. `knowledge_graph_entity` is an optional secondary identifier — a [knowledge graph entity](/knowledge-graph/entities) cross-reference attached to some people — not the Person itself; rely on `id`/`slug` to identify a guest. The `stats` block is the guest-specific part: * `appearances_by_bias` buckets appearances by the political-bias rating of the host show. Keys include `NOT_POLITICAL` (the dominant bucket for most guests, since most shows aren't political) alongside `EXTREME_LEFT … EXTREME_RIGHT`, so the political mix is contextualized. Counts sum to at most `appearance_count` — appearances on shows whose bias hasn't been evaluated are omitted. * `appearances_by_suitability_tier` does the same for the IAB Tech Lab brand-suitability tier (`SAFE`, `LIMITED`, `SENSITIVE`, `UNSAFE`). * `top_podcasts` is the (up to five) most-frequent shows, each with the guest's per-show appearance count and the show's `suitability_tier` and `bias`. A guest profile returns **404** when the Person exists but has never appeared as a guest, so a 200 is itself confirmation of guest activity. ## Appearances `GET /v1/podcasts/guests/{id}/appearances` lists the episodes a guest appeared on, most recent first. ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/guests/brad-gerstner/appearances" \ --data-urlencode "min_speaking_seconds=120" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "data": [ { "episode": { "id": "1lYfNtoWjotDDGoK77IAc4", "title": "Crypto Fueling a War", "slug": "crypto-fueling-a-war-2", "published_at": "2026-05-22T17:16:40Z", "duration_seconds": 2296 }, "podcast": { "id": "6aSPee2tS89h0BMiIjlA6X", "title": "Armstrong & Getty Podcast", "slug": "armstrong-and-getty", "image_url": "https://cdn.particle.pro/url/media/d876513c-…" }, "speaking_seconds": 345.95, "suitability_tier": "SAFE", "bias": "RIGHT" } ], "has_more": true, "cursor": "r.4gfFC6" } ``` Each row pairs the `episode` with the `podcast` that published it, the guest's identified `speaking_seconds` on that episode, and the show's `suitability_tier` and `bias`. ### Filters | Param | Notes | | -------------------------------------- | -------------------------------------------------------------------- | | `podcast_id` | Slug or ID. Restrict to appearances on one podcast. | | `topic_id` | Restrict to appearances on episodes under this topic. | | `published_after` / `published_before` | ISO 8601 dates bounding the episode publication window. | | `suitability_tier_max` | `SAFE`, `LIMITED`, `SENSITIVE`, `UNSAFE`. | | `min_speaking_seconds` | Only appearances with identified speaking time above this threshold. | ## Podcasts a guest has joined `GET /v1/podcasts/guests/{id}/podcasts` returns the distinct set of shows a guest has appeared on, with a per-show rollup. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/guests/brad-gerstner/podcasts" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "data": [ { "podcast": { "id": "4t9PU1WkzOroEXfBl6ia7r", "title": "All-In with Chamath, Jason, Sacks & Friedberg", "slug": "all-in", "image_url": "https://cdn.particle.pro/url/media/a0a7b2fa-…" }, "appearance_count": 19, "first_appearance_at": "2020-11-04T10:06:21Z", "last_appearance_at": "2026-05-08T22:16:00Z", "suitability_tier": "SAFE", "bias": "LEANS_RIGHT" } ], "has_more": false } ``` This is the aggregate behind `top_podcasts` on the profile, but unbounded and paginated — every show, not just the top five. ## Guest roster for a podcast `GET /v1/podcasts/{id}/guests` is the inverse pivot: the identified guests of a *given show*. Here `{id}` is a **podcast** reference. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/all-in/guests" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "data": [ { "id": "5njEX8HDc7oNdM8kr9ItRs", "slug": "brad-gerstner", "name": "Brad Gerstner", "appearance_count": 19, "distinct_podcasts": 10, "first_appearance_at": "2020-11-04T10:06:21Z", "last_appearance_at": "2026-05-08T22:16:00Z" } ], "has_more": true, "cursor": "r.4gfFC6" } ``` The rows are the same compact guest shape as the directory, but here `appearance_count` is scoped to *this show* (Brad Gerstner's 19 appearances on All-In), while `distinct_podcasts` stays his lifetime count across the catalog. Returns **404** when the podcast can't be resolved. ## Trending guests `GET /v1/podcasts/guests/trends` surfaces guests who are currently making the rounds — the cross-show interview activity that signals a book launch, product unveil, news-cycle moment, or new-on-the-scene debut. ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/guests/trends" \ --data-urlencode "min_distinct_podcasts=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "data": [ { "guest": { "id": "57kYjDofrt8lUZgIDz4qMb", "slug": "david-epstein", "name": "David Epstein", "appearance_count": 30, "distinct_podcasts": 29, "last_appearance_at": "2026-05-25T07:00:00Z" }, "recent_appearances": 20, "first_appearance_in_window": "2026-05-04T07:03:00Z", "last_appearance_at": "2026-05-25T07:00:00Z" } ], "has_more": true, "cursor": "r.4gfFC7" } ``` `guest` carries lifetime stats; `recent_appearances` is the count *inside the window*. Every in-window appearance is on a distinct show — guests with repeats on the same show inside the window are excluded — so the number doubles as "distinct podcasts this window." This is **not** a leaderboard of perennial regulars. Recurring co-hosts and daily news-segment contributors are filtered out by design: a guest must show activity materially elevated above their own baseline, across multiple distinct shows, with substantive (5+ minute) interviews. For a steady-state directory, use [`GET /v1/podcasts/guests`](#the-guest-directory) instead. ### Filters | Param | Notes | | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `since` | ISO 8601 date. Inclusive lower bound of the window. Default: 30 days ago. | | `min_distinct_podcasts` | Minimum distinct shows inside the window. Minimum 2, default 2. | | `first_appearance_since` | Restrict to guests whose earliest-ever appearance is on or after this date — the "new on the scene" cut. | | `topic_id` | Restrict to in-window appearances on episodes under this topic. | | `suitability_tier_max` | `SAFE`, `LIMITED`, `SENSITIVE`, `UNSAFE`. | ## Brand-suitability exposure `GET /v1/podcasts/guests/{id}/suitability` returns the distribution of IAB Tech Lab brand-suitability tiers across the shows a guest has appeared on, plus the categories most often flagged in those shows. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/guests/brad-gerstner/suitability" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "lifetime": { "tier_distribution": [ { "tier": "SAFE", "episodes": 28, "distinct_podcasts": 6 }, { "tier": "LIMITED", "episodes": 5, "distinct_podcasts": 2 }, { "tier": "SENSITIVE", "episodes": 2, "distinct_podcasts": 2 } ] }, "recent_90d": { "window_start": "2026-03-03T02:50:50Z", "tier_distribution": [ { "tier": "SAFE", "episodes": 11, "distinct_podcasts": 4 } ] }, "top_categories": [ { "code": "death_injury_military_conflict", "prevalence": "FREQUENT", "treatment": "DOCUMENTARY", "podcasts": 6 }, { "code": "debated_social_issues", "prevalence": "FREQUENT", "treatment": "EDITORIAL", "podcasts": 4 } ], "notes": "This response measures the guest's exposure across the shows they have appeared on. The brand-safety tier of a show reflects the show's overall content profile and is not a verdict on any individual guest. Appearing on a SENSITIVE- or UNSAFE-tier show does not imply the guest is unsuitable." } ``` Both a `lifetime` and a trailing `recent_90d` window are returned; each buckets the guest's appearances by tier with episode and distinct-podcast counts. `top_categories` lists the most-flagged IAB categories with their `prevalence` (`INCIDENTAL`, `OCCASIONAL`, `FREQUENT`, `PERVASIVE`) and dominant `treatment`. This endpoint measures *exposure*, not a verdict on the guest. A show's brand-safety tier reflects the show's overall content profile — appearing on a `SENSITIVE`- or `UNSAFE`-tier show does not imply the guest is unsuitable. The `notes` field restates this in every response. ## Pagination The list endpoints use the standard envelope: `limit` (1–100, default 25) plus an opaque `cursor`, returning `{ data, has_more, cursor }`. Pass the `cursor` from one response back as `?cursor=…` to fetch the next page. The single-item endpoints — the profile and the suitability exposure — are not paginated. ## Related * [Mentions](/podcasts/mentions) — every line where a person or company is named, host or guest. * [Brand Suitability](/podcasts/suitability) — the per-show assessment behind a guest's exposure profile. * [Episodes](/podcasts/episodes) — episode discovery, including `entity_id` recall by speaker. * [Knowledge graph entities](/knowledge-graph/entities) — the optional `knowledge_graph_entity` cross-reference attached to some guests. # Lookup by external identifier Source: https://docs.particle.pro/podcasts/lookup Resolve Apple, Spotify, YouTube, and other platform identifiers to Particle podcasts — deterministically, in bulk, with each result echoing the input you sent. If you already have a corpus of podcasts identified by their **Apple collection ID**, **Spotify show ID**, **YouTube channel ID**, **RSS feed URL**, or any other platform-native identifier, the regular catalog endpoints aren't the right tool to reconcile them with Particle's catalog: * `GET /v1/podcasts?q=…` is a fuzzy *name* search. It can't find a show whose title you're spelling differently than we do — and there's no guarantee the top hit is the right podcast even when names look similar. * `GET /v1/podcasts/{id}` accepts our internal ID, the Particle slug, or a numeric Apple/iTunes collection ID — but not Spotify, YouTube, or other third-party IDs. The **lookup** endpoint closes that gap. Send it a `platform` plus up to 100 platform-native identifiers, and it returns one record per input — each echoing the identifier you sent alongside the matched Particle podcast (or omitting the `podcast` key when the identifier doesn't resolve). The same data backs [external links](/podcasts/external-links); this endpoint lets you query it in the opposite direction — from platform identifier to podcast. Use this endpoint when you want to: * Reconcile an existing corpus of podcasts (a CMS export, a chart scrape, a partner feed) with Particle's catalog by Apple/Spotify/etc. IDs. * Map a **Spotify show ID**, **YouTube channel ID**, or other third-party identifier into our `podcast.id` before calling any other Particle endpoint. (A numeric Apple/iTunes collection ID can be passed straight into the `{id}` slot of any podcast endpoint — no lookup needed.) * Bulk-resolve a list of identifiers without losing the input → output correlation that a fuzzy filter on `/v1/podcasts` can't give you. ## Look up by Apple Podcasts ID ```bash curl theme={"dark"} curl -G "https://api.particle.pro/v1/podcasts/lookup" \ -H "X-API-Key: $PARTICLE_API_KEY" \ --data-urlencode "platform=apple" \ --data-urlencode "identifier=1535809341,1200361736" ``` ```js JavaScript theme={"dark"} const params = new URLSearchParams({ platform: "apple", identifier: "1535809341,1200361736", }); const res = await fetch( `https://api.particle.pro/v1/podcasts/lookup?${params}`, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { results } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/lookup", params={"platform": "apple", "identifier": "1535809341,1200361736"}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) results = res.json()["results"] ``` ```jsonc Response theme={"dark"} { "results": [ { "identifier": "1535809341", "podcast": { "id": "QpMz7GYKfSNuUa6zKXA4Q", "title": "All-In with Chamath, Jason, Sacks & Friedberg", "slug": "all-in-with-chamath-jason-sacks-friedberg", "image_url": "https://cdn.particle.pro/podcasts/all-in.jpg" } }, { "identifier": "1200361736" // no `podcast` key — this Apple ID didn't resolve } ] } ``` That's the whole shape. Each result echoes the input `identifier` so you can join the response back to your input list by position *or* by key — your call. Unresolved identifiers omit the `podcast` field entirely; clients should test for key presence rather than a `null` value, matching the convention used elsewhere in the API for nullable nested resources. ## Look up by RSS feed URL The `rss` platform is the odd one out: it doesn't live in `external-links` because every podcast already carries its own canonical feed URL on the podcast itself. Pass the URL exactly as it appears on the podcast's RSS feed: ```bash curl theme={"dark"} curl -G "https://api.particle.pro/v1/podcasts/lookup" \ -H "X-API-Key: $PARTICLE_API_KEY" \ --data-urlencode "platform=rss" \ --data-urlencode "identifier=https://feeds.simplecast.com/54nAGcIl" ``` ```js JavaScript theme={"dark"} const params = new URLSearchParams({ platform: "rss", identifier: "https://feeds.simplecast.com/54nAGcIl", }); const res = await fetch( `https://api.particle.pro/v1/podcasts/lookup?${params}`, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { results } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/lookup", params={"platform": "rss", "identifier": "https://feeds.simplecast.com/54nAGcIl"}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) results = res.json()["results"] ``` Comparison is **byte-exact**: a `?param` suffix, a trailing slash, or `http://` vs `https://` will not match if our stored URL is the other form. If you don't have the URL exactly as the publisher serves it, fall back to looking up by Apple/Spotify/YouTube ID instead — those are more forgiving to normalise. ## Parameters | Field | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `platform` | **Required.** Platform slug — any value from the [external-links platform reference](/podcasts/external-links#platform-reference), plus two in-house aliases: `itunes` (treated as `apple`) and **`rss`** (match by canonical RSS feed URL — see [Look up by RSS feed URL](#look-up-by-rss-feed-url) above). | | `identifier` | **Required.** One or more platform-native identifiers, comma-separated. Up to **100 per call**. Duplicates in your input are deduped silently — you'll get one result per unique input. | A few details worth knowing up front: * **Order is preserved.** Results appear in the request order of their first occurrence. If you send `?identifier=A,B,A`, you get two results — A then B — not three. * **Exact matching only.** Identifiers are compared exactly; case differences and prefix mismatches do not resolve. Leading and trailing whitespace is stripped automatically so a copy-pasted ID with stray spaces still works. * **`platform=bogus` is a 400.** Unknown platform slugs return an error listing every accepted slug — same vocabulary as the external-links responses. ## Looking up the same podcast across platforms The endpoint takes one platform at a time. To resolve a multi-platform corpus, fan out one call per platform — each call covers up to 100 identifiers and the responses are independent: ```bash curl theme={"dark"} # Apple curl -G "https://api.particle.pro/v1/podcasts/lookup" \ -H "X-API-Key: $PARTICLE_API_KEY" \ --data-urlencode "platform=apple" \ --data-urlencode "identifier=1535809341" # Spotify curl -G "https://api.particle.pro/v1/podcasts/lookup" \ -H "X-API-Key: $PARTICLE_API_KEY" \ --data-urlencode "platform=spotify" \ --data-urlencode "identifier=5BiqcD1FSCwlgZTcljpAYi" # YouTube curl -G "https://api.particle.pro/v1/podcasts/lookup" \ -H "X-API-Key: $PARTICLE_API_KEY" \ --data-urlencode "platform=youtube" \ --data-urlencode "identifier=UCESLZhusAkFfsNsApnjF_Cg" # RSS feed URL (matches against the canonical podcast feed URL) curl -G "https://api.particle.pro/v1/podcasts/lookup" \ -H "X-API-Key: $PARTICLE_API_KEY" \ --data-urlencode "platform=rss" \ --data-urlencode "identifier=https://feeds.simplecast.com/54nAGcIl" ``` Once you have the resolved `podcast.id` for each input, hit [`GET /v1/podcasts/{id}/external-links`](/podcasts/external-links) to see every other platform that podcast also lives on. That's the natural follow-up when you want to enrich rather than reconcile. ## Behavior and edge cases ### Apple Podcasts and iTunes share IDs Apple Podcasts uses a single numeric collection ID across both its modern and iTunes-era surfaces. The lookup endpoint reflects that: `platform=apple` and `platform=itunes` are aliases that behave identically — either form finds the same matches. Because Apple/iTunes IDs are always numeric, you can also pass one **straight into the `{id}` slot** of any podcast endpoint — e.g. `GET /v1/podcasts/1535809341` — and skip this lookup entirely. The `{id}` slot resolves iTunes IDs the same way this lookup does — including the [highest-charting tiebreaker](#ambiguous-identifiers-resolve-to-the-highest-charting-podcast) for shared identifiers — so both always return the same podcast. ### Misses omit the `podcast` field Unmatched identifiers come back as `{"identifier": "…"}` with no `podcast` key. This is intentional — OpenAPI clients now see an accurate schema where `podcast` is optional, and absence is a single sentinel for "no match" without the ambiguity of explicit `null`. ```jsonc theme={"dark"} // Matched { "identifier": "1535809341", "podcast": { "id": "…", "title": "…", "slug": "…", "image_url": "…" } } // Unmatched { "identifier": "9999999999" } ``` ### Duplicates collapse If your input list contains duplicates, the response contains one entry per unique identifier — in the order each one first appeared. We do this silently rather than echoing duplicates back, so your client doesn't have to dedupe twice. ### Compact response only The matched podcast is returned in the compact shape (`id`, `title`, `slug`, `image_url`) — enough to pivot to any other endpoint, but without the fuller metadata that [`GET /v1/podcasts/{id}`](/podcasts/overview) returns. For each identifier you actually care about, follow up with the detail endpoint using the returned `id` (or `slug`). ### Ambiguous identifiers resolve to the highest-charting podcast A few platform identifiers in the wild are claimed by more than one podcast — most often when a publisher exposes the *same* YouTube channel as the video presence for several shows (e.g. the parent New York Times YouTube channel is listed under The Daily, Serial, The Headlines, Matter of Opinion, The Book Review, and others). When that happens, the lookup returns the podcast with the highest current Apple Podcasts / Spotify chart ranking, so the same input always resolves to the same output across calls. If you need to find *every* podcast a shared identifier belongs to, that's an [external-links](/podcasts/external-links) question, not a lookup one — start from each candidate podcast and inspect its external-link rows. ### 100-identifier cap Calls with more than 100 identifiers are rejected with a 400. The cap is generous enough that most reconciliations fit in a single round-trip; if you need more, fan out in chunks of 100. (The cap is per platform — you can do multiple platform calls in parallel.) ## Choosing the right endpoint | You want to… | Use this | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Resolve a known **Apple / Spotify / YouTube / …** ID to a Particle podcast | **`GET /v1/podcasts/lookup`** *(this page)* | | Resolve a **RSS feed URL** to a Particle podcast | **`GET /v1/podcasts/lookup?platform=rss`** *(this page)* | | Search by podcast **name** (fuzzy text match) | [`GET /v1/podcasts?q=…`](/podcasts/overview) | | Get the **full** podcast detail object (publisher, language, episode count, bias, suitability tier) | [`GET /v1/podcasts/{id}`](/podcasts/overview) | | Enumerate every platform a Particle podcast lives on | [`GET /v1/podcasts/{id}/external-links`](/podcasts/external-links) | | Search **dialogue** inside episodes (not podcasts) | [`GET /v1/podcasts/episodes/search`](/podcasts/search) | ## Related * [External links](/podcasts/external-links) — given a Particle podcast, list every third-party platform identifier we have for it; the opposite direction of this endpoint. * [Podcasts overview](/podcasts/overview) — full Podcast object and all sub-resources. * [Concepts](/concepts) — pagination, pricing weights, and authentication. # Mentions Source: https://docs.particle.pro/podcasts/mentions Every line of dialogue where a person or company is named, grouped by episode. The right endpoint for read-everything-about-X workflows. `GET /v1/podcasts/mentions` returns every line of dialogue across the catalog where a resolved entity is named, grouped by episode and ordered by recency. Each item in `data` is one episode plus all of its mention windows. A *mention window* is a contiguous range of dialogue around a name, with `is_mention` flagged on the lines that actually contain it. This is the right endpoint when the answer doesn't depend on the wording of your query — only on **who** is being named. Available to MCP agents as [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions). ## When to use Mentions vs Search | You want… | Use this | | ----------------------------------------------------------------- | ---------------------------------------------------------------------- | | **Every line where a person or company is mentioned** | [`/v1/podcasts/mentions?entity_id=…`](/podcasts/mentions) | | Episodes featuring an entity, restricted to one podcast | [`/v1/podcasts/mentions?entity_id=…&podcast_id=…`](/podcasts/mentions) | | Mentions filtered by participation (host vs guest vs mention) | [`/v1/podcasts/mentions?entity_id=…&role=guest`](/podcasts/mentions) | | Dialogue *about a topic* across the catalog (paraphrase tolerant) | [`/v1/podcasts/episodes/search?semantic_search=…`](/podcasts/search) | | Dialogue containing exact tokens or phrases | [`/v1/podcasts/episodes/search?keyword_search=…`](/podcasts/search) | ## Examples ```bash theme={"dark"} # Every line about Sam Altman across the catalog, with 2 lines of context. curl --get "https://api.particle.pro/v1/podcasts/mentions" \ --data-urlencode "entity_id=sam-altman" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```bash theme={"dark"} # Only lines where Sam Altman appears as a guest. curl --get "https://api.particle.pro/v1/podcasts/mentions" \ --data-urlencode "entity_id=sam-altman" \ --data-urlencode "role=guest" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```bash theme={"dark"} # Mentions of OpenAI inside a single podcast. curl --get "https://api.particle.pro/v1/podcasts/mentions" \ --data-urlencode "company_id=openai" \ --data-urlencode "podcast_id=all-in" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Response ```json theme={"dark"} { "entity": {"id": "…", "slug": "sam-altman", "name": "Sam Altman"}, "data": [ { "episode": { "id": "…", "title": "…", "published_at": "2026-04-28T10:00:00Z", "podcast": {"id": "…", "title": "All-In"} }, "mention_count": 8, "mention_variants": ["Sam Altman", "Altman"], "windows": [ { "segment": {"id": "…", "type": "INTERVIEW", "title": "…"}, "start_seconds": 942.0, "end_seconds": 988.5, "lines": [ {"number": 137, "speaker": "Kara Swisher", "role": "HOST", "start_seconds": 942.0, "end_seconds": 948.2, "text": "…"}, {"number": 138, "speaker": "Scott Galloway", "role": "HOST", "start_seconds": 948.5, "end_seconds": 955.1, "text": "…", "is_mention": true}, {"number": 139, "speaker": "Kara Swisher", "role": "HOST", "start_seconds": 955.5, "end_seconds": 988.5, "text": "…"} ] } ] } ], "has_more": true, "cursor": "s.AbCd…" } ``` One episode = one item in `data`. `windows[]` is ordered by time within the episode. `mention_count` is the number of lines that triggered `is_mention=true`. `mention_variants` is the distinct strings observed for the entity (e.g., `["Sam Altman", "Altman"]`) — useful for UI labels. If an episode mentions the entity more than 50 times, the page surfaces the first 50 windows and sets `truncated: true` on the episode. The next-page cursor still advances to a different episode — windows never split across pages. ## Filters | Param | Notes | | ----------------- | ------------------------------------------------------------- | | `entity_id` | Slug or ID. Required when `company_id` is unset. | | `company_id` | Slug, domain, or ID. Resolves to the company's linked entity. | | `podcast_id` | Restrict to mentions in one podcast. | | `episode_id` | Restrict to a single episode. | | `role` | `guest`, `host`, `panelist`, `correspondent`, `mention`. | | `since` / `until` | Episode `published_at` window. ISO date. | | `context_lines` | 0–20, default 2. Lines of surrounding dialogue per window. | ## Pagination Standard `limit` (1–100, default 25) + opaque `cursor`. **The unit of pagination is the episode** — a single episode never splits across pages, so a page may return fewer total dialogue lines than another with the same limit. Pass the `cursor` from the previous response back as `?cursor=…` for the next page. ## Related * [Search](/podcasts/search) — find dialogue by topic or phrase. * [Transcripts → mentions in one episode](/podcasts/transcripts#transcript-mentions) — every entity mentioned in one episode (the inverse view). * [Podcast → mentions rollup](/podcasts/overview#mentions-across-a-podcasts-episodes) — episode-level salience aggregated across one podcast. # Podcasts Source: https://docs.particle.pro/podcasts/overview What's inside every episode — transcripts, speakers, segments, clips, sponsors, and bias. Most podcast APIs give you metadata — titles, descriptions, RSS feeds. Particle API gives you what's *in* the audio. Every episode is transcribed and diarized, broken into structural segments (intros, ads, topic discussions, interviews), distilled into engagement-scored clips, and linked to a knowledge graph of speakers, entities, and topics. The shows themselves are scored for political bias with structured evidence. ## What you can build * **Entity tracking** — Find every episode, segment, and dialogue line where a person or company is discussed. * **Highlight reels** — Pull engagement-scored clips with ready-to-share `intro_statement` copy and direct MP3 URLs. * **Sponsor analytics** — See which podcasts a competitor advertises on, who co-sponsors with whom, and host-read vs pre-recorded breakdowns. * **Research tools** — Search transcripts with timestamps, exports as JSON or SRT, scoped by speaker or time range. * **Bias analysis** — Audit a show's leaning with structured `transcript_evidence` and `web_research_evidence`. ## Resource hierarchy ```mermaid theme={"dark"} graph TD Pub[Publisher] --> P[Podcast] P --> E[Episodes] P --> B[Bias] P --> Adv[Advertising summary] P --> M[Mentions] E --> T[Transcript] E --> Seg[Segments] E --> Cl[Clips] E --> Sp[Speakers] E --> En[Entities] E --> To[Topics] E --> TM[Transcript mentions] Seg --> SegT[Transcript] Cl --> ClT[Transcript] ``` Every resource is reachable both scoped to its parent (e.g., segments for a specific episode) and across the catalog (e.g., all segments matching a filter). Heavier endpoints — full transcripts, transcript mentions, cross-catalog segments and clip search, and advertising analytics — are priced higher per call than light metadata lookups; see [Concepts → Pricing weight](/concepts#pricing-weight). Cross-podcast discovery, filtering, and per-episode sub-resources. The people who appear on shows — lifetime profiles, appearances, trending press tours, and brand-suitability exposure. What kind of show it is — guest frequency, interview/panel/call-in/solo formats, ads, video, episode length, and cadence — all filterable. Diarized dialogue, word-level timestamps, SRT export, and entity mentions in context. AI-identified structural sections and engagement-scored highlight moments. Sponsors, leaderboards, co-occurrence, and per-company ad presence. Every directory, social profile, video channel, and website the podcast lives on — with resolved URLs and audience metrics. Reverse the external-links index: resolve Apple, Spotify, YouTube, and other platform identifiers to Particle podcasts in bulk. Browse the organizations behind the catalog and pivot from a publisher to its full lineup of podcasts. User-generated 1–5 star ratings and review text, per-platform star histograms, and a periodic LLM-generated narrative summary of listener sentiment. ## Get a podcast Resolve by slug, canonical ID, or numeric Apple/iTunes collection ID: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/pivot" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch("https://api.particle.pro/v1/podcasts/pivot", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); const podcast = await res.json(); ``` ```json Response from GET /v1/podcasts/pivot (truncated) theme={"dark"} { "id": "QpMz7GYKfSNuUa6zKXA4Q", "title": "Pivot", "slug": "pivot", "language": "en", "episode_count": 167, "bias": "LEANS_LEFT", "political_context": "US", "speakers": [ { "name": "Kara Swisher", "role": "HOST", "occurrences": 164, "entity_slug": "kara-swisher" }, { "name": "Scott Galloway", "role": "HOST", "occurrences": 136, "entity_slug": "scott-galloway" } ] } ``` The `speakers` array is the recurring cast aggregated across episodes. Each `entity_slug` is a knowledge-graph handle you can pass to [entities endpoints](/knowledge-graph/entities) or to [`GET /v1/podcasts/episodes?entity_id=…`](/podcasts/episodes#list-episodes) to follow that speaker across the whole catalog. Already have a platform-native ID? A numeric **Apple/iTunes collection ID** can be passed straight into the `{id}` slot — e.g. `GET /v1/podcasts/1535809341` — no lookup needed. For a **Spotify show ID**, **YouTube channel ID**, **RSS feed URL**, or other platform identifier, use [`GET /v1/podcasts/lookup`](/podcasts/lookup) to deterministically resolve any of them (or up to 100 in a single call) to Particle podcasts. ### Search by name If you only know the show by name, list podcasts with a fuzzy `q=` filter — it's case-insensitive and matches title and description: ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts" \ --data-urlencode "q=hard fork" \ --data-urlencode "limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` The response is a paginated list of compact podcast objects — same `PageResponse` envelope used by every list endpoint, ranked by match quality. `q=` is a *ranked text search*, not a deterministic mapping: the top hit is usually right but isn't guaranteed to be. If you need a deterministic answer keyed on a platform-native identifier (Apple, Spotify, YouTube, …), use [`GET /v1/podcasts/lookup`](/podcasts/lookup) instead. ### Curate by popularity and topic The list endpoint is also a discovery tool. Filter by `topic_id`, `language`, or `suitability_tier`, and narrow to the most popular shows with `popularity_threshold`. Currently-charting podcasts carry a `popularity` field — a **global** popularity percentile in `(0,1]` (a `cume_dist` ranking over every charting podcast, where `1.0` is the single most popular show). It is omitted for podcasts not currently on any chart. `popularity_threshold` filters on that same value (and, because non-charting podcasts have no percentile, excludes them whenever it is set): ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts" \ --data-urlencode "topic_id=sports/football/fantasy-football" \ --data-urlencode "popularity_threshold=0.75" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` This returns the top-quartile-popularity podcasts where fantasy football is a substantive share of the show — the `topic_id` filter requires the topic to carry at least 20% of a podcast's episodes and ranks by topic concentration. `popularity_threshold` is **always global**: it ranks across all charting podcasts, never within `topic_id` or any other filter. Podcasts that aren't currently charting have no percentile and are excluded whenever the filter is set, so a high threshold on a narrow topic can return nothing — lower or drop `popularity_threshold` to widen the set. This pairs naturally with the [episode feed](/podcasts/feed): curate a list here, then hand its podcast IDs to the feed as an explicit, stable `podcast_ids` set instead of the feed's dynamic popularity stream. ## Bias analysis Every podcast carries a quick `bias` enum (`LEANS_LEFT`, `LEANS_RIGHT`, `CENTER`, `MIXED`, `UNCLEAR`) on the podcast resource. The bias endpoint returns the underlying analysis: structured reasoning, transcript-level evidence, web-research evidence, and the sample episodes that drove the conclusion. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/pivot/bias" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "result": "LEANS_LEFT", "political_context": "US", "confidence": "MEDIUM", "reasoning": "…", "transcript_evidence": "…", "web_research_evidence": "…", "sample_episode_ids": ["…", "…"], "episodes_analyzed": 15, "evaluated_at": "2026-04-09T17:31:06Z" } ``` Use the result for filtering or tagging; surface the `reasoning` and `transcript_evidence` fields when users want to know *why*. ## Mentions across a podcast's episodes To find every episode of a single podcast where an entity comes up — without iterating episodes yourself — use the podcast-scoped mentions endpoint: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/pivot/mentions?entity_id=sam-altman&limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```json Response (truncated) theme={"dark"} { "data": [ { "episode": { "id": "78cgekLUjCJBUZbj3s5K8Y", "title": "WHCD Shooting Aftermath, Musk and Altman Face-Off, Spirit Airlines Bailout", "published_at": "2026-04-28T10:00:00Z", "has_transcript": true, "segment_count": 21, "clip_count": 8 }, "salience": 0.003, "occurrences": 8 } ], "has_more": true, "cursor": "…" } ``` For the dialogue lines around each mention, drill into one episode with [transcript mentions](/podcasts/transcripts#transcript-mentions). ## Choosing the right endpoint | I want to… | Use this | | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Resolve a podcast from a third-party identifier (Apple, Spotify, YouTube, …) | [`GET /v1/podcasts/lookup?platform=…&identifier=…`](/podcasts/lookup) | | Fuzzy-find a podcast by **name** when I only know what it's called | [`GET /v1/podcasts?q=…`](/podcasts/overview#get-a-podcast) | | Get the full podcast detail object when I already have a Particle slug or ID | [`GET /v1/podcasts/{id}`](/podcasts/overview#get-a-podcast) | | Find dialogue that *means* something — paraphrase-tolerant | [`GET /v1/podcasts/episodes/search?semantic_search=…`](/podcasts/search) | | Find dialogue containing exact tokens or phrases (BM25) | [`GET /v1/podcasts/episodes/search?keyword_search=…`](/podcasts/search) | | Find **every line about a person or company** | [`GET /v1/podcasts/mentions?entity_id=…`](/podcasts/mentions) | | Combine: ranked dialogue scoped to a person or company | [`GET /v1/podcasts/episodes/search?semantic_search=…&entity_id=…`](/podcasts/search) | | List every entity mentioned inside one episode | [`GET /v1/podcasts/episodes/{id}/transcript/mentions`](/podcasts/transcripts#transcript-mentions) | | Salience rollup of an entity across one podcast's episodes | [`GET /v1/podcasts/{id}/mentions?entity_id=…`](#mentions-across-a-podcasts-episodes) | | Episode-level recall by entity (metadata only, no dialogue) | [`GET /v1/podcasts/episodes?entity_id=…`](/podcasts/episodes) | | Find the people who **appeared** on shows, with appearance history and stats | [`GET /v1/podcasts/guests`](/podcasts/guests) | | Filter shows by **format** — interview vs solo, guest frequency, length, cadence, ad-free | [`GET /v1/podcasts?guest_frequency=…&format_signal=…`](/podcasts/format) | | Browse highlight clips ranked by engagement | [`GET /v1/podcasts/clips`](/podcasts/segments-and-clips) — for clips that match dialogue content, use `/v1/podcasts/episodes/search` and read them off the result. | | Browse publishers, or list every podcast a publisher produces | [`GET /v1/podcasts/publishers`](/podcasts/publishers) | The split between **Search** and **Mentions** is deliberate: Search ranks dialogue by relevance to a query (similarity, BM25, or hybrid). Mentions returns episodes containing a named entity, ordered by recency, with every dialogue line where that name appears. Different shapes for genuinely different questions — see the [Search](/podcasts/search) and [Mentions](/podcasts/mentions) pages for full guidance. ## Related * [Search](/podcasts/search) — find dialogue by topic or exact phrase (semantic, keyword, hybrid) * [Mentions](/podcasts/mentions) — every line where a person or company is named * [Guests](/podcasts/guests) — the people who appear on shows, their appearances, trends, and suitability exposure * [Format profile](/podcasts/format) — guest frequency, detected formats, ads, video, episode length, and cadence as filterable attributes * [Episodes](/podcasts/episodes) — discovery, filtering, sub-resources * [Transcripts](/podcasts/transcripts) — dialogue, words, mentions, SRT * [Segments & clips](/podcasts/segments-and-clips) — structural breakdown and highlight moments * [Advertising](/podcasts/advertising) — sponsor analytics * [External links](/podcasts/external-links) — directories, social profiles, websites with resolved URLs and audience metrics * [Lookup by external ID](/podcasts/lookup) — reverse the external-links index for Apple, Spotify, YouTube, and other platform identifiers * [Publishers](/podcasts/publishers) — browse organizations and pivot from a publisher to its catalog * [Ratings & reviews](/podcasts/ratings) — user-generated star ratings, review text, per-platform aggregates, and narrative sentiment summaries # Publisher Brand Suitability Source: https://docs.particle.pro/podcasts/publisher-suitability Publisher-level rollups of IAB Tech Lab Brand Safety & Suitability Framework verdicts — tier composition, per-category exposure, cross-publisher leaderboards, and category-pivot views for regulated brands. The per-podcast [brand suitability assessment](/podcasts/suitability) is the unit of measurement; the publisher rollups described here are the unit of *decision*. Advertisers don't buy a single show — they buy across a publisher's catalog. A buying decision needs a per-publisher composition view, a way to rank publishers against each other, and a way to pivot on the categories that matter for a specific brand. These endpoints expose four views over the same underlying assessments: 1. **`GET /v1/podcasts/publishers/{id}/suitability`** — the composite **profile** for one publisher. Tier composition, per-category exposure, top concerns. The headline endpoint. 2. **`GET /v1/podcasts/publishers/{id}/suitability/podcasts`** — drill down into a publisher to see which specific podcasts are concerning, filterable by tier, category, prevalence, and treatment. 3. **`GET /v1/podcasts/suitability/publishers/leaderboard`** — cross-publisher ranking by safest, riskiest, most-placeable, or most-analyzed. 4. **`GET /v1/podcasts/suitability/categories/{code}/publishers`** — pivot on a single GARM category and rank publishers by exposure. The killer view for regulated brands (alcohol, gambling, cannabis, family). Per-podcast endpoints — `GET /v1/podcasts/{id}/suitability` and the embedded `suitability_tier` on `GET /v1/podcasts/{id}` — remain the source of truth for an individual show. The publisher endpoints aggregate from those same assessments. ## Coverage caveat Suitability analyses are still being backfilled across the catalog. At time of writing, \~10% of the catalog has been assessed, biased toward more popular shows. Every aggregate response surfaces a `coverage.analyzed_coverage` (0..1 share) and a `coverage.quality` enum (`LOW` \< 0.3, `MEDIUM` 0.3–0.7, `HIGH` ≥ 0.7) so callers can weight the rollup against the underlying sample. Cross-publisher rankings additionally accept a `min_analyzed_podcasts` parameter (default `5`) so a publisher with one or two analyzed shows cannot dominate share-based rankings. ## Identifying a publisher The publisher-scoped endpoints (`/v1/podcasts/publishers/{id}/...`) accept the same publisher identifier as the rest of the [publisher endpoints](/podcasts/publishers): a slug (recommended — `iheartpodcasts`, `bbc-radio-4`, `bloomberg`) or the publisher ID. ## Publisher profile The composite profile is the one-call summary an advertiser asks for first: "is this publisher's catalog broadly placeable?" ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/publishers/bloomberg/suitability" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/publishers/bloomberg/suitability", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const profile = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/publishers/bloomberg/suitability", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) profile = res.json() ``` The response carries the full picture in one round-trip: * **`tier_breakdown`** — counts and shares of `SAFE`, `LIMITED`, `SENSITIVE`, `UNSAFE` across the publisher's analyzed catalog, plus a `placeable_share` (`SAFE` + `LIMITED`) — the "broadly buyable" slice. * **`category_breakdown`** — one entry per GARM category, always 12 entries even when prevalence is `NONE`, with prevalence counts, advertiser-relevant treatment counts (`PROMOTIONAL`, `GLAMORIZING`), and a `high_risk_count` (FREQUENT/PERVASIVE or GLAMORIZING). * **`top_concerns`** — up to three categories ranked by `high_risk_count`. Surfaced separately so callers can scan a publisher's worst dimensions without traversing the full breakdown. * **`confidence_breakdown`** — `LOW` / `MEDIUM` / `HIGH` counts of the agent verdicts that produced this rollup. * **`coverage.analyzed_coverage` + `coverage.quality`** — context for the composition above. ## Drill down to specific podcasts When the profile shows concerning composition, drill into the specific shows. The drill-down endpoint returns a paginated list of the publisher's analyzed podcasts with their tier, confidence, and flagged categories. ```bash theme={"dark"} # Most-risky shows in the iHeart catalog curl "https://api.particle.pro/v1/podcasts/publishers/iheartpodcasts/suitability/podcasts?sort=risk_desc&limit=10" \ -H "X-API-Key: $PARTICLE_API_KEY" # iHeart shows where 'illegal_drugs_alcohol_tobacco' content is at least OCCASIONAL curl "https://api.particle.pro/v1/podcasts/publishers/iheartpodcasts/suitability/podcasts?category=illegal_drugs_alcohol_tobacco&min_prevalence=OCCASIONAL" \ -H "X-API-Key: $PARTICLE_API_KEY" # iHeart shows that GLAMORIZE 'crime_harmful_acts' content curl "https://api.particle.pro/v1/podcasts/publishers/iheartpodcasts/suitability/podcasts?category=crime_harmful_acts&treatment=GLAMORIZING" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Each entry surfaces only enough per-category context to make the row scan-able (`code`, `prevalence`, `treatment`, derived `risk_level`). Per-category reasoning and evidence excerpts are intentionally not surfaced here — fetch `GET /v1/podcasts/{id}/suitability` for full per-category detail on a specific show. Sort options: * **`risk_desc`** *(default)* — `UNSAFE` first, then `SENSITIVE`, `LIMITED`, `SAFE`. * **`risk_asc`** — `SAFE` first. * **`recently_evaluated`** — most-recent assessment first; useful when you want to surface shows whose verdicts were just refreshed. ## Cross-publisher leaderboard Rank publishers against each other on one of four metrics: | Metric | Sorted by | | ---------------- | --------------------------------------------- | | `safest` | `SAFE` share of the analyzed catalog (desc) | | `riskiest` | `UNSAFE` share of the analyzed catalog (desc) | | `most_placeable` | combined `SAFE` + `LIMITED` share (desc) | | `most_analyzed` | absolute `analyzed_podcasts` count (desc) | ```bash theme={"dark"} # Top 25 publishers with at least 5 analyzed shows, ranked safest first curl "https://api.particle.pro/v1/podcasts/suitability/publishers/leaderboard?metric=safest&min_analyzed_podcasts=5&limit=25" \ -H "X-API-Key: $PARTICLE_API_KEY" # Publishers most exposed to UNSAFE-tier shows curl "https://api.particle.pro/v1/podcasts/suitability/publishers/leaderboard?metric=riskiest" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` The leaderboard reflects each publisher's *current* tier composition (sourced from the denormalized tier on each podcast). It does not accept a historical date window — paginate the audit history per show via `GET /v1/podcasts/{id}/suitability?include=history` if you need point-in-time analysis. Each row carries the publisher's `tier_breakdown`, `coverage.analyzed_podcasts`, `coverage.analyzed_coverage` + `coverage.quality`, and `metric_value` (the actual sort key — a share \[0, 1] for `safest` / `riskiest` / `most_placeable`, an absolute count for `most_analyzed`). ## Category pivot — the regulated-brand view The most differentiated view in this surface. For one of the 12 GARM categories, rank publishers by exposure across their catalog. An alcohol advertiser pivots on `illegal_drugs_alcohol_tobacco` and immediately sees which publishers carry the highest catalog-level exposure (and at what prevalence + treatment); a family brand pivots on `adult_sexual` or `hate_speech_aggression` to build an exclusion list. ```bash theme={"dark"} # Publishers most exposed to illegal_drugs_alcohol_tobacco content curl "https://api.particle.pro/v1/podcasts/suitability/categories/illegal_drugs_alcohol_tobacco/publishers" \ -H "X-API-Key: $PARTICLE_API_KEY" # Same, but only count PROMOTIONAL exposures (sponsored CBD, alcohol endorsements) curl "https://api.particle.pro/v1/podcasts/suitability/categories/illegal_drugs_alcohol_tobacco/publishers?treatment=PROMOTIONAL" \ -H "X-API-Key: $PARTICLE_API_KEY" # Publishers where 'debated_social_issues' content is at least OCCASIONAL curl "https://api.particle.pro/v1/podcasts/suitability/categories/debated_social_issues/publishers?min_prevalence=OCCASIONAL" \ -H "X-API-Key: $PARTICLE_API_KEY" # Reverse view — publishers *least* exposed to a category curl "https://api.particle.pro/v1/podcasts/suitability/categories/adult_sexual/publishers?direction=least_exposed" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Each row in the response surfaces: * `exposure_share` — share of the publisher's analyzed catalog with any non-`NONE` prevalence in the category (or above `min_prevalence` when set, restricted to `treatment` when set). * `high_risk_count` — FREQUENT/PERVASIVE or GLAMORIZING podcasts in the category. * `prevalence_breakdown` and `treatment_breakdown` — the full distribution of exposures in this category across the publisher's catalog. * `example_podcasts` — up to three example podcasts from the publisher's catalog that exhibit the highest exposure (ranked PERVASIVE → INCIDENTAL, GLAMORIZING → DOCUMENTARY, then by ID for determinism) so the abstract shares anchor to concrete shows you recognize. ## Tier classification recap The publisher rollups use the same four-tier verdict as the per-podcast assessment: | Tier | Framework risk level | Advertiser fit | | ----------- | -------------------- | ------------------------------------------------------------------------------------ | | `SAFE` | Low Risk | Suitable for all advertisers, including kid-directed and family brands. | | `LIMITED` | Medium Risk | Suitable for most advertisers; family brands may apply caution. | | `SENSITIVE` | High Risk | Many brands skip; suitable for general-audience and adult-product advertisers. | | `UNSAFE` | Brand Safety Floor | No monetization — content violates the Brand Safety Floor for at least one category. | See the [per-podcast suitability docs](/podcasts/suitability) for the full GARM category set, the prevalence + treatment axes, and the risk-derivation rules. # Publishers Source: https://docs.particle.pro/podcasts/publishers Browse the organizations behind the podcasts — Goalhanger, iHeartPodcasts, BBC Radio 4 — and pivot from a publisher to its full catalog. A **publisher** is the organization that produces and distributes a podcast — Goalhanger, iHeartPodcasts, BBC Radio 4, Wondery, NPR. The publisher endpoints let you browse the full set of publishers in the catalog, look one up by a stable slug, and pivot from a publisher to every podcast attributed to it. The same publisher record also appears embedded as a `publisher` object on the podcast detail response (`GET /v1/podcasts/{id}`), so you usually don't need a separate lookup to display a podcast's publisher — these endpoints exist for browsing and reverse navigation. Use these endpoints when you want to: * Build a "browse by publisher" view of the catalog. * Resolve a publisher slug from a URL into its podcasts. * Rank publishers by catalog size, or list them alphabetically. * Cross-reference a podcast's publisher with the rest of its catalog. ## Identifying a publisher Every publisher endpoint that takes an `{id}` path parameter accepts either form: * **Slug** (recommended): a stable, human-readable identifier — `goalhanger`, `iheartpodcasts`, `bbc-radio-4`. Slugs are populated for the vast majority of publishers and returned on every publisher response. * **ID**: the publisher ID. Always works as a fallback for the small number of publishers whose name doesn't slugify (e.g. names that aren't representable as ASCII URL slugs). Slugs and IDs are interchangeable in the URL — pick whichever you have. ## List publishers ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/publishers?sort=podcast_count&limit=10" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/publishers?sort=podcast_count&limit=10", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data, has_more, cursor } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/publishers", params={"sort": "podcast_count", "limit": 10}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) body = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "0RZ7y1ABC", "slug": "iheartpodcasts", "name": "iHeartPodcasts", "podcast_count": 612 }, { "id": "0RZ7y1DEF", "slug": "wondery", "name": "Wondery", "podcast_count": 184 }, { "id": "0RZ7y1GHI", "slug": "bbc-radio-4", "name": "BBC Radio 4", "podcast_count": 142 } // … ], "has_more": true, "cursor": "eyJvIjoxMH0=" } ``` ### Query parameters | Parameter | Default | Description | | --------- | --------------- | --------------------------------------------------------------------------------------------- | | `sort` | `podcast_count` | `podcast_count` ranks publishers by catalog size, largest first. `name` sorts alphabetically. | | `limit` | `25` | Page size, 1–100. | | `cursor` | — | Opaque cursor from the previous response's `cursor` field. | ## Get a publisher ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/publishers/goalhanger" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/publishers/goalhanger", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const publisher = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/publishers/goalhanger", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) publisher = res.json() ``` ```jsonc Response theme={"dark"} { "id": "0RZ7y1XYZ", "slug": "goalhanger", "name": "Goalhanger", "podcast_count": 14 } ``` Returns 404 when no publisher matches the supplied slug or ID. ## List podcasts for a publisher Pivot from a publisher to its full catalog, ordered by popularity: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/publishers/goalhanger/podcasts?limit=5" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/publishers/goalhanger/podcasts?limit=5", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data, has_more, cursor } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/publishers/goalhanger/podcasts", params={"limit": 5}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) body = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "0RZ7y1AAA", "slug": "the-rest-is-history", "title": "The Rest Is History", "language": "en", // …other Podcast fields }, { "id": "0RZ7y1BBB", "slug": "the-rest-is-politics", "title": "The Rest Is Politics", "language": "en" // … } // … ], "has_more": false } ``` Returns 404 when no publisher matches the supplied slug or ID. The response contains the same `Podcast` objects returned by `GET /v1/podcasts` — see the [podcasts overview](/podcasts/overview) for the full field reference. ## Anatomy of a publisher | Field | Description | | --------------- | ------------------------------------------------------------------------------------------------- | | `id` | Stable publisher identifier. Always present. | | `slug` | Human-readable identifier. Recommended in URLs. Omitted on publishers whose name doesn't slugify. | | `name` | Publisher's display name. | | `podcast_count` | Number of podcasts in the catalog attributed to this publisher. | The same publisher also appears in compact form (`id`, `slug`, `name` only) as the `publisher` field on the podcast detail response — you do not need a separate publisher lookup just to display a podcast's publisher in the UI. ## Choosing the right endpoint | You want to… | Use this | | --------------------------------------------- | ---------------------------------------------------------------------- | | Browse all publishers, ranked by catalog size | `GET /v1/podcasts/publishers?sort=podcast_count` | | Browse all publishers alphabetically | `GET /v1/podcasts/publishers?sort=name` | | Resolve a publisher slug from a URL | `GET /v1/podcasts/publishers/{slug}` | | List every podcast a publisher produces | `GET /v1/podcasts/publishers/{id}/podcasts` | | Get the publisher of a specific podcast | The `publisher` field on [`GET /v1/podcasts/{id}`](/podcasts/overview) | ## Related * [Podcasts overview](/podcasts/overview) — full Podcast object and other sub-resources. * [Episodes](/podcasts/episodes) — drill from a podcast into its individual episodes. * [External links](/podcasts/external-links) — third-party platform presences for a podcast. # Podcast Publisher Bias Source: https://docs.particle.pro/podcasts/publishers-bias Aggregate political bias intelligence at the podcast publisher level: catalog-wide lean, diversity, regional distribution, and cross-publisher rankings. Particle API analyzes every well-classified podcast against a seven-point political bias scale, calibrated against a regional political framework (US, UK, EU, CANADA, AUSTRALIA, INDIA, or OTHER). The publisher-level endpoints described here roll those per-podcast verdicts up across every podcast a publisher controls, so advertisers, researchers, and platforms can reason about a publisher's full footprint rather than evaluating shows one at a time. Publisher-level bias intelligence is a Premium endpoint. The per-podcast bias rating remains available on the standard tier through [GET /v1/podcasts//bias](/podcasts/overview#bias-analysis). ## What the profile gives you A bundle-buy decision (or a publisher-fit audit) needs more than the modal lean of a single show. Particle's publisher bias profile captures the catalog along five dimensions: * **Coverage** (`coverage` block) — `total_podcasts`, `analyzed_podcasts`, and `analyzed_coverage` so callers know how much of the catalog the verdict draws from. Backfill is in flight; a value of `coverage.analyzed_coverage` materially below 1.0 means there are still un-analyzed shows. * **Political content** (`political_content` block) — `political_content.share` is the fraction of analyzed podcasts the agent rated as political (i.e. not `NOT_POLITICAL`). Distinguishes news-heavy publishers from entertainment-heavy publishers. * **Lean** (`lean` block, absent from the response when the publisher has no political content) — `lean.avg_score` on a –3 (extreme left) to +3 (extreme right) ordinal scale, restricted to political content. * **Diversity** — `lean.stddev`, the standard deviation of the lean across political podcasts. Low values indicate a monolithic publisher (e.g. The Daily Wire, MeidasTouch); high values indicate a heterogeneous catalog (iHeartPodcasts spans EXTREME\_RIGHT to LEFT). * **Distributions** (`distributions` block) — bucket-level counts across the seven directional buckets plus `NOT_POLITICAL`, and a per-region breakdown by political\_context so callers can audit the verdict directly. ## The bias scale | Bucket | Score | Description | | --------------- | ----- | ----------------------------------------------- | | `EXTREME_LEFT` | –3 | Far-left content with overt partisan framing. | | `LEFT` | –2 | Clearly left-leaning editorial perspective. | | `LEANS_LEFT` | –1 | Center-left orientation; mild partisan signal. | | `CENTER` | 0 | Even-handed treatment of political topics. | | `LEANS_RIGHT` | +1 | Center-right orientation; mild partisan signal. | | `RIGHT` | +2 | Clearly right-leaning editorial perspective. | | `EXTREME_RIGHT` | +3 | Far-right content with overt partisan framing. | | `NOT_POLITICAL` | — | Not substantively political content. | The numeric score is exposed so callers can sort, average, and visualize across podcasts; it is not a substitute for the literal bucket and `NOT_POLITICAL` content is excluded from score-based metrics. ## Get a publisher's bias profile ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/publishers/iheartpodcasts/bias" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/publishers/iheartpodcasts/bias", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const profile = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/publishers/iheartpodcasts/bias", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) profile = res.json() ``` ```jsonc Response theme={"dark"} { "publisher": { "id": "...", "slug": "iheartpodcasts", "name": "iHeartPodcasts" }, "coverage": { "total_podcasts": 210, "analyzed_podcasts": 85, "analyzed_coverage": 0.4048 }, "political_content": { "podcasts": 17, "share": 0.20, "avg_topic_percentage": 0.41 }, "lean": { // absent when political_content.podcasts == 0 "avg_score": -0.47, "stddev": 1.62, "dominant_bias": "LEANS_LEFT" }, "distributions": { "bias": [ { "result": "NOT_POLITICAL", "count": 68 }, { "result": "LEANS_LEFT", "count": 9 }, { "result": "LEFT", "count": 4 }, { "result": "RIGHT", "count": 3 }, { "result": "EXTREME_RIGHT", "count": 1 } ], "region": [ { "political_context": "UNKNOWN", "count": 68 }, { "political_context": "US", "count": 17 } ] }, "last_evaluated_at": "2026-04-12T18:33:21Z" } ``` The response is intentionally grouped: `coverage` is "how much of the catalog the verdict draws from", `political_content` is "how political the catalog is", and `lean` is "where the political content sits on the ideological axis". When a publisher has no political content at all (e.g. a sports-only network), the `lean` block is absent from the response — a single `"lean" in obj` check answers "is there a lean signal at all?". Leaderboard entries share the same nested shape minus `distributions`. ## List a publisher's analyzed podcasts Drill into the underlying podcasts that drove the profile. Filter by bias bucket(s) (comma-separated) or `political_context`, exclude NOT\_POLITICAL podcasts, and sort by lean, recency, or name. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/publishers/npr/bias/podcasts?bias=LEFT,LEANS_LEFT&sort=lean_score&order=asc" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Sort options: | Sort | Description | | -------------- | -------------------------------------------------------- | | `lean_score` | -3..+3 ordinal lean. Default. | | `evaluated_at` | Most recent analysis first (or oldest with `order=asc`). | | `name` | Alphabetical. | ## Cross-publisher leaderboard Rank publishers by a chosen bias metric. Useful for discovery (what are the most right-leaning podcast publishers in the US?) and reporting (which publishers are most catalog-diverse?). Score-based metrics gate small-sample publishers via `min_analyzed_podcasts` and `min_political_podcasts`. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/bias/publishers/leaderboard?metric=most_right_leaning&political_context=US&min_political_podcasts=5" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Available metrics: | Metric | Description | | -------------------- | ----------------------------------------------------------------------------------------------------- | | `most_left_leaning` | Lowest `lean.avg_score` first. Requires ≥ `min_political_podcasts` political podcasts. | | `most_right_leaning` | Highest `lean.avg_score` first. Requires ≥ `min_political_podcasts` political podcasts. | | `most_political` | Highest `political_content.share` first. Distinguishes news publishers from entertainment publishers. | | `most_diverse` | Highest `lean.stddev` first — publishers whose catalog spans a wide political range. | | `most_monolithic` | Lowest `lean.stddev` first — publishers whose catalog is tightly clustered around one lean. | | `most_analyzed` | Highest `coverage.analyzed_coverage` first — publishers whose catalog is closest to fully analyzed. | ## Flip view: which publishers carry the most podcasts in a given bias bucket? The symmetric "given a bucket, which publishers?" lens. Useful when the question is "who carries the most RIGHT-leaning content?" rather than "where does publisher X stand?". ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/bias/RIGHT/publishers?sort=share&min_count=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` `sort=count` ranks by raw `podcasts_in_bucket`; `sort=share` ranks by `podcasts_in_bucket / analyzed_podcasts`. Use `share` to surface publishers whose catalog is concentrated in the bucket regardless of catalog size; `count` to surface the largest absolute footprints. Every entry includes up to five `sample_podcast_ids` so callers can spot-check the verdict without an extra request. ## Methodology notes * **Latest analysis only.** Every aggregate is computed from the most recent non-deleted analysis per podcast (`DISTINCT ON (podcast_id) ORDER BY evaluated_at DESC`). Historical analyses are kept for audit but never contribute to the rolled-up profile. * **NOT\_POLITICAL is not 0.** `NOT_POLITICAL` content is excluded from `lean.avg_score`, `lean.stddev`, and `lean.dominant_bias`. Including it as a `CENTER` proxy would silently muddy the lean signal for entertainment-heavy publishers. * **Sample standard deviation.** `lean.stddev` is the sample standard deviation (Bessel's correction, `n−1`). It is `null` when there is fewer than two political podcasts. * **Coverage caveat.** Analysis backfill is in flight. Treat `coverage.analyzed_coverage` as a confidence proxy on the rolled-up verdict — publishers with low coverage should be re-checked once the backfill completes. # Rankings Source: https://docs.particle.pro/podcasts/rankings Browse the live Apple and Spotify podcast charts — top podcasts by country and category, per-podcast chart presence, historical snapshots, and trending movers. The rankings endpoints expose podcast chart data captured daily from Apple Podcasts and Spotify. We refresh **roughly 1.7 million chart positions every day** across 170+ countries × 100+ categories × ranks 1–200, so the data you query is always the live snapshot — yesterday's charts are already historical and addressable through the history endpoints. Every chart slot is identified by the same four-field vocabulary — **source**, **chart\_type**, **country**, **category\_slug** — and that vocabulary works the same way on every endpoint. Learn it once and you know the whole surface. The simplest call needs no parameters at all: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/rankings" -H "X-API-Key: $PARTICLE_API_KEY" ``` returns the first page of the **US Apple Top Podcasts** chart, ordered by rank ascending. Charts run to rank 200; the default page size is 25 (raise it up to 100 with `limit`, or paginate with `cursor`). Use these endpoints when you want to: * Render a chart leaderboard for any country or category. * See where a specific podcast currently ranks across every source/country/category. * Track historical chart positions over time. * Surface trending podcasts — chart debuts, departures, and the biggest rank movers. ## Identifying a chart slot A "chart slot" is the unit of analysis: one (source × chart\_type × country × category) tuple, refreshed daily. Every browse and history endpoint accepts the same query parameters to pin or scope a slot: | Parameter | Default | Description | | --------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- | | `source` | `apple` | Ranking source platform: `apple` or `spotify`. | | `chart_type` | `top_podcasts` | Chart variant within the source. Currently only `top_podcasts` is available. | | `country` | `us` | Lowercase ISO 3166-1 alpha-2 country code (e.g. `us`, `gb`, `jp`). | | `category_slug` | — | Category slug (e.g. `comedy`, `business`). Universal across sources. **Omit for the overall "Top Podcasts" chart.** | Use the [discovery endpoints](#discovery-endpoints) to enumerate the available sources, countries, and categories. ## List rankings The workhorse list endpoint. With no parameters it returns the US Apple Top Podcasts overall chart. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/rankings" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch("https://api.particle.pro/v1/podcasts/rankings", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); const { data, has_more, cursor } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/rankings", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) body = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "0RZ7y1DAY01", "source": "apple", "chart_type": "top_podcasts", "country": "us", "rank": 1, "chart_total": 200, "captured_at": "2026-05-09T03:07:21Z", "is_current": true, "external_id": "1581410252", "external_url": "https://podcasts.apple.com/us/podcast/the-daily/id1581410252", "show": { "name": "The Daily", "publisher": "The New York Times", "language": "en-US", "image_url": "https://is1-ssl.mzstatic.com/.../600x600.jpg", "total_episodes": 1842, "genres": [{ "external_id": "1489", "name": "News" }] }, "podcast": { "id": "0RZ7y1ABC", "slug": "the-daily", "title": "The Daily", "image_url": "https://cdn.particle.pro/podcasts/the-daily.jpg" } } // …25 entries (the default page size; charts run to rank 200, paginate with `cursor`) ], "has_more": true, "cursor": "r.AQAAAGQ" } ``` Common shapes of the call: | Goal | Call | | ------------------------------------ | ----------------------------------------------------------- | | UK Apple Top Comedy | `GET /v1/podcasts/rankings?country=gb&category_slug=comedy` | | US Spotify Top Podcasts | `GET /v1/podcasts/rankings?source=spotify` | | Just the top 10 of any chart | `GET /v1/podcasts/rankings?max_rank=10` | | Ranks 11–20 of a chart | `GET /v1/podcasts/rankings?min_rank=11&max_rank=20` | | Where does a specific podcast chart? | `GET /v1/podcasts/rankings?podcast_id=the-daily` | | Where does it chart in the UK? | `GET /v1/podcasts/rankings?podcast_id=the-daily&country=gb` | ### Query parameters | Parameter | Default | Description | | --------------- | -------------- | ----------------------------------------------------------------------------------------------------------------- | | `source` | `apple` | Source platform. | | `chart_type` | `top_podcasts` | Chart variant. | | `country` | `us` | Country code. | | `category_slug` | — | Category slug. Omit for the overall chart. | | `podcast_id` | — | Slug, ID, or numeric iTunes ID. Restricts results to one podcast (and skips the implicit "overall only" default). | | `min_rank` | — | Optional inclusive rank floor (1–500). | | `max_rank` | — | Optional inclusive rank ceiling (1–500). | | `limit` | `25` | Page size, 1–100. | | `cursor` | — | Opaque cursor from the previous response. | ### About the linked podcast We track tens of thousands of the highest-ranked podcasts in the catalog — every show that consistently charts is in there, with full transcripts, sponsors, mentions, and the rest of the Particle Pro surface. Those rows carry a `podcast` field that links straight into the catalog (`GET /v1/podcasts/{slug}` and friends). For the long tail of intermittent or hyper-local entries — chart slots in 170+ countries surface a lot of one-day wonders — `podcast` is omitted and the response still carries a complete `show` block with name, publisher, artwork, and feed URL. Treat `show` as the canonical identity for every row and use `podcast` to cross-link into the catalog whenever it's present. ## List current rankings for a podcast Returns every live chart slot a podcast appears on, across sources and countries. Rows are clustered by (source, country, category) — overall charts come before sub-categories within each (source, country) group — so iterating the response naturally walks the podcast's footprint without re-sorting. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-daily/rankings" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/the-daily/rankings", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "source": "apple", "country": "us", "category": null, "rank": 1, "captured_at": "2026-05-09T03:07:21Z", /* … */ }, { "source": "apple", "country": "us", "category": { "slug": "news", "name": "News", "external_id": "1489" }, "rank": 1, /* … */ }, { "source": "apple", "country": "gb", "category": null, "rank": 4, /* … */ }, { "source": "spotify", "country": "us", "category": null, "rank": 5, /* … */ } // …more chart slots ], "has_more": false } ``` Returns 404 when the podcast can't be resolved by slug, ID, or numeric iTunes ID. Pagination is opt-in via `limit` and `cursor`; by default every chart slot the podcast occupies is returned in one response (typically dozens, occasionally a few hundred for very popular shows charting in many countries). ## Summarize a podcast's chart presence A one-call aggregate: how many distinct chart slots, sources, countries, and categories the podcast is on right now, plus its single best (lowest-numbered) rank. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-daily/rankings/summary" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response theme={"dark"} { "podcast": { "id": "0RZ7y1ABC", "slug": "the-daily", "title": "The Daily" }, "chart_count": 47, "source_count": 2, "country_count": 18, "category_count": 4, "best_rank": { "source": "apple", "chart_type": "top_podcasts", "country": "us", "rank": 1, "captured_at": "2026-05-09T03:07:21Z" }, "appearances_by_source": [ { "source": "apple", "chart_count": 35, "best_rank": 1, "best_country": "us", "best_category_slug": "" }, { "source": "spotify", "chart_count": 12, "best_rank": 4, "best_country": "us", "best_category_slug": "" } ] } ``` When multiple chart placements tie at the same rank, `best_rank` (and each `appearances_by_source[*].best_*`) prefers the higher-priority market — `us > gb > ca > au > de > fr > es > it > mx > br > jp > ie > nl > se > nz`, then alphabetical. A US #1 always wins over a tied #1 in a smaller market. Within a country, the overall chart wins over a sub-category placement. ## Discovery endpoints These let you enumerate which sources, countries, and categories actually have data right now — without paginating through millions of rows. ### List sources `GET /v1/podcasts/rankings/sources` returns each `(source, chart_type)` pair currently available, with row counts and freshness: ```jsonc theme={"dark"} { "data": [ { "source": "apple", "chart_type": "top_podcasts", "current_row_count": 1737573, "latest_captured_at": "2026-05-09T03:07:21Z" }, { "source": "spotify", "chart_type": "top_podcasts", "current_row_count": 11100, "latest_captured_at": "2026-05-09T06:14:56Z" } ], "has_more": false } ``` ### List countries `GET /v1/podcasts/rankings/countries` returns every country with current data. Each entry carries the human-readable name (when known) and the count of distinct chart slots. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/rankings/countries?source=apple" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc theme={"dark"} { "data": [ { "code": "us", "name": "United States", "current_chart_count": 18, "sources": ["apple", "spotify"] }, { "code": "gb", "name": "United Kingdom", "current_chart_count": 17, "sources": ["apple"] } // … ], "has_more": false } ``` ### List categories `GET /v1/podcasts/rankings/categories` returns every category currently represented. For Apple sub-categories the response includes `parent_slug` so you can render the hierarchy. ```jsonc theme={"dark"} { "data": [ { "slug": "society-culture", "name": "Society & Culture", "external_id": "1324", "current_chart_count": 172, "sources": ["apple"] }, { "slug": "self-improvement", "name": "Self-Improvement", "external_id": "1500", "parent_slug": "education", "current_chart_count": 172, "sources": ["apple"] }, { "slug": "comedy", "name": "Comedy", "external_id": "1303", "current_chart_count": 198, "sources": ["apple", "spotify"] } // … ], "has_more": false } ``` `external_id` is the source-native category id. When a category is reported by multiple sources (Apple's iTunes genre id "1303" and Spotify's slug-as-id "comedy" both map to `comedy`), the response prefers Apple's id; pin a single source via `?source=spotify` if you need the Spotify-native form. `category_slug` is the universal handle and works as a filter on every endpoint regardless of source. ## History History endpoints reach beyond the live snapshot to surface chart positions over time. ### Chart slot history Returns historical snapshots for a chart slot, ordered most-recent first. Combine with `since` / `until` to bound the range. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/rankings/history?country=us&since=2026-04-01" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Each row in the response is a `PodcastRanking` object — the same shape as the list endpoint, just with older `captured_at` values. Pin `podcast_id` to filter to a single podcast within the slot. ### Per-podcast history Returns the full historical chart record for a single podcast, optionally narrowed by chart slot or date range. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-daily/rankings/history?country=us&since=2026-01-01" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` When `category_slug` is set on this endpoint, `source` must also be set (returns 422 otherwise) — category slugs aren't unique across sources at the wire level, so a Spotify "comedy" filter would otherwise silently exclude Apple matches and vice versa. ## Movers `GET /v1/podcasts/rankings/movers` returns the chart entries whose rank changed between the live snapshot and a comparison snapshot `window_days` ago. Use it to surface trending movement, new arrivals, and departures — all from one endpoint. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/rankings/movers?country=us&window_days=1" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/rankings/movers?country=us&window_days=1", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "source": "apple", "chart_type": "top_podcasts", "country": "us", "rank": 4, "show": { "name": "Smartless", "publisher": "Wondery" }, "change": "up", "delta": 9, // rose from #13 to #4 "previous_snapshot": { "rank": 13, /* full PodcastRanking shape — same fields as the row above, omitted for brevity */ } }, { "source": "apple", "country": "us", "rank": 7, "show": { "name": "New Show", "publisher": "Indie" }, "change": "new" // chart debut }, { "source": "apple", "country": "us", "rank": 200, // last seen rank "show": { "name": "Stale Show", "publisher": "…" }, "change": "exit" // fell off the chart } ] } ``` The `change` filter narrows to a single change type: | `change` | Meaning | | --------------- | ----------------------------------------------------------------- | | `all` (default) | Every change type below. Stable rows are always excluded. | | `up` | Entries whose rank improved (lower number). | | `down` | Entries whose rank dropped. | | `new` | Entries that appeared on the chart since the comparison snapshot. | | `exit` | Entries that fell off the chart. | ### Query parameters | Parameter | Default | Description | | -------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `source`, `chart_type`, `country`, `category_slug` | `apple`, `top_podcasts`, `us`, — | Standard chart-slot filters. | | `window_days` | `1` | Comparison window in days, 1–30. The comparison snapshot is the most recent retired snapshot closest in time to (now − window\_days). | | `change` | `all` | Change-type filter. | | `limit` | `25` | Maximum movers to return, 1–100. | ## Anatomy of a ranking row | Field | Description | | ------------------- | ----------------------------------------------------------------------------------------------- | | `id` | Stable identifier for this specific snapshot of this entry. | | `source` | `apple` or `spotify`. | | `chart_type` | `top_podcasts`. | | `country` | Lowercase ISO 3166-1 alpha-2 code. | | `category` | `{slug, name, external_id, parent_slug}` or null for the overall chart. | | `rank` | 1-based position. | | `previous_rank` | Source-reported prior rank when the source provides it. Apple does not currently populate this. | | `growth_indicator` | Source-supplied trend hint (e.g. `up`, `down`). | | `chart_total` | Length of the chart at this snapshot (typically 200). | | `captured_at` | When we captured the snapshot (UTC). Stable for every row in the same snapshot. | | `source_updated_at` | Timestamp the source itself reported for the chart, when present. | | `external_id` | Source-native chart entry id. Stable across captures. | | `external_url` | Canonical URL on the source platform. | | `show` | Show metadata captured with the entry. **Always present**, even when `podcast` is absent. | | `podcast` | Compact podcast object when matched in our catalog; absent otherwise. | ## Choosing the right endpoint | You want to… | Use this | | --------------------------------------- | ------------------------------------------------ | | The current US Apple Top Podcasts | `GET /v1/podcasts/rankings` | | Top podcasts in another country | `GET /v1/podcasts/rankings?country=gb` | | Top podcasts in a category | `GET /v1/podcasts/rankings?category_slug=comedy` | | Where one podcast charts right now | `GET /v1/podcasts/{id}/rankings` | | One-call podcast popularity card | `GET /v1/podcasts/{id}/rankings/summary` | | What sources/countries/categories exist | The three discovery endpoints | | Historical snapshots of a chart | `GET /v1/podcasts/rankings/history` | | Historical rank of a podcast | `GET /v1/podcasts/{id}/rankings/history` | | Trending movers / debuts / departures | `GET /v1/podcasts/rankings/movers` | ## Related * [Podcasts overview](/podcasts/overview) — full Podcast object. * [Publishers](/podcasts/publishers) — pivot from a chart entry's publisher to its catalog. * [Episodes](/podcasts/episodes) — drill from a charting podcast into its episodes. * [Ratings & reviews](/podcasts/ratings) — user-generated star ratings and review text, the complement to chart position. # Ratings & reviews Source: https://docs.particle.pro/podcasts/ratings Per-podcast user-generated 1–5 star ratings and review text, a numeric aggregate per (platform, locale), and a periodic narrative summary of listener sentiment. The ratings endpoints expose user-generated **ratings** (1–5 stars) and **reviews** (optional free-text title and body) from third-party platforms. Today the source is **Apple Podcasts customer reviews**, covering the `us`, `gb`, `ca`, `au`, `nz`, and `ie` storefronts. These endpoints surface **listener opinion**. For **chart position** (where a show ranks on Apple/Spotify Top Podcasts) use [`/v1/podcasts/rankings`](/podcasts/rankings) instead. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-daily/ratings" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` returns the most recent ratings for the podcast across every platform and locale, newest first. Use these endpoints when you want to: * Display a podcast's review history with star scores, titles, and body text. * Render the per-platform star average + histogram on a podcast detail page. * Pull a short narrative summary of what listeners are saying — without parsing hundreds of reviews yourself. * Compare reception across countries (e.g. US vs UK reviews for the same show). ## Anatomy of a rating | Field | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------- | | `id` | Stable identifier for this rating. | | `platform_slug` | Canonical platform identifier. Today only `apple`. | | `locale` | Platform-specific locale code (e.g. Apple storefront `us`, `gb`). Empty for platforms without a locale dimension. | | `platform_rating_id` | Platform-native identifier for the rating. | | `stars` | Integer 1–5 star score. | | `title` | Review title when present. | | `body` | Review body when present. | | `author_display_name` | Display name the reviewer chose on the platform. | | `author_uri` | Reviewer profile URL on the platform, when present. | | `posted_at` | RFC 3339 timestamp of when the rating was posted on the source. | | `link_href` | Source-platform link for the individual rating, when present. | Optional fields are omitted when no data is available. ## List ratings for a podcast Returns the most recent ratings for the podcast, newest first. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-daily/ratings" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/the-daily/ratings", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data, has_more, cursor } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/the-daily/ratings", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) body = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "0RZ7y1DAY01", "platform_slug": "apple", "locale": "us", "platform_rating_id": "14110653821", "stars": 5, "title": "Required daily listening", "body": "I've started every morning with this since 2018…", "author_display_name": "morningcommuter", "author_uri": "https://itunes.apple.com/us/reviews/id17563210", "posted_at": "2026-05-26T22:02:40Z", "link_href": "https://itunes.apple.com/us/review?id=1535809341&type=Podcast" }, { "id": "0RZ7y1DAY02", "platform_slug": "apple", "locale": "gb", "platform_rating_id": "14109001233", "stars": 2, "title": "Ad-heavy lately", "body": "Loved the format for years but the ad load has crept up…", "author_display_name": "tubebreezeway", "posted_at": "2026-05-26T11:14:08Z" } // …25 entries by default; paginate with `cursor` ], "has_more": true, "cursor": "r.AQAAAGQ" } ``` ### Query parameters | Parameter | Default | Description | | --------------- | ------- | -------------------------------------------------------- | | `platform_slug` | — | Restrict to one platform. | | `locale` | — | Restrict to one platform locale (Apple storefront code). | | `min_stars` | — | Lower bound (inclusive) on the star score, 1–5. | | `since` | — | Lower bound on `posted_at` (RFC 3339). | | `until` | — | Upper bound on `posted_at` (RFC 3339). | | `limit` | `25` | Page size, 1–100. | | `cursor` | — | Opaque cursor from the previous response. | ## Summarize a podcast's ratings A one-call aggregate: the star average and histogram for each (platform, locale) the podcast is rated on, a combined cross-source roll-up, plus the latest narrative summary when available. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-daily/ratings/summary" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/the-daily/ratings/summary", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const summary = await res.json(); ``` ```jsonc Response theme={"dark"} { "podcast_id": "0RZ7y1ABC", "entries": [ { "platform_slug": "apple", "locale": "us", "rating_count": 78492, "rating_average": 4.59, "histogram": { "stars_1": 1820, "stars_2": 1456, "stars_3": 3712, "stars_4": 13105, "stars_5": 58399 }, "computed_at": "2026-05-27T18:14:02Z" }, { "platform_slug": "apple", "locale": "gb", "rating_count": 6418, "rating_average": 4.43, "histogram": { "stars_1": 188, "stars_2": 220, "stars_3": 412, "stars_4": 1390, "stars_5": 4208 }, "computed_at": "2026-05-27T18:14:02Z" } // …one entry per (platform, locale) the podcast has been rated on ], "combined": { "platform_slug": "", "locale": "", "rating_count": 94782, "rating_average": 4.59, "histogram": { "stars_1": 2154, "stars_2": 1798, "stars_3": 4350, "stars_4": 15842, "stars_5": 70638 }, "computed_at": "2026-05-27T18:14:02Z" }, "sentiment": { "platform_slug": "apple", "locale": "", "summary_text": "Listeners consistently praise the host's calm pacing and the show's reliable five-day cadence; the most common recent criticism is a perceived uptick in ad load. A small but vocal contingent has been frustrated with topic selection over the past two months. Reviews remain overwhelmingly positive overall.", "window_start": "2026-04-28T00:00:00Z", "window_end": "2026-05-26T22:02:40Z", "rating_count_in_window": 312, "generated_at": "2026-05-27T08:00:00Z" } } ``` `entries` carries one row per (platform, locale) the podcast has been rated on, or `null` when the podcast hasn't been rated yet. `combined` is the count-weighted roll-up across every entry — `platform_slug` and `locale` are empty to signal the cross-source view — and is `null` when there's nothing to aggregate. `sentiment` is a short narrative summary (typically 2–5 sentences) over the most recent ratings, intended for non-technical readers — analysts, brand and PR teams, talent agents, journalists, producers. It paraphrases what listeners are saying rather than quoting individual reviews, and is null when one hasn't been generated yet (e.g. on freshly ingested podcasts or shows with very few ratings). Returns 404 when the podcast can't be resolved by slug, ID, or numeric iTunes ID. ### Sentiment summary fields | Field | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------- | | `summary_text` | The narrative paragraph. | | `window_start` | Earliest `posted_at` in the ratings window the summary covers. | | `window_end` | Latest `posted_at` in the ratings window the summary covers. | | `rating_count_in_window` | How many ratings the summary aggregates. | | `generated_at` | When the summary was produced. | | `platform_slug`, `locale` | Scope of the summary. Today summaries are platform-scoped (Apple) and cross-locale (empty `locale`). | ## Choosing the right endpoint | You want to… | Use this | | ------------------------------------------------------------------------ | ------------------------------------------------------------------ | | Show a podcast's review history with stars, titles, and text | `GET /v1/podcasts/{id}/ratings` | | Render a star average + histogram per platform | `GET /v1/podcasts/{id}/ratings/summary` | | Render a single overall star average (across every platform/locale) | `GET /v1/podcasts/{id}/ratings/summary` → `combined` | | Surface a short narrative of what listeners are saying lately | `GET /v1/podcasts/{id}/ratings/summary` → `sentiment` | | Get chart positions (Apple/Spotify rankings) | [`GET /v1/podcasts/rankings`](/podcasts/rankings) | | Get the podcast's own metadata (title, publisher, description, RSS feed) | [`GET /v1/podcasts/{id}`](/podcasts/overview) | | Pull a podcast's third-party platform IDs and audience sizes | [`GET /v1/podcasts/{id}/external-links`](/podcasts/external-links) | ## Related * [Podcasts overview](/podcasts/overview) — full Podcast object and the other sub-resources. * [Rankings](/podcasts/rankings) — Apple and Spotify chart positions; the complementary signal to user ratings. * [External links](/podcasts/external-links) — platform-level audience-size attributes (followers, subscribers) on Spotify, YouTube, etc. # Search Source: https://docs.particle.pro/podcasts/search Search the podcast catalog by what is said in episodes — by meaning, by exact phrase, or both. For 'every line about a person or company', see Mentions. `GET /v1/podcasts/episodes/search` finds dialogue inside podcast episodes. Each result is a segment of an episode, returned with bounded transcript windows centered on the highest-relevance dialogue lines (flagged `is_match: true`) and any highlight clips that overlap the segment. Available to MCP agents as [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts). This endpoint searches **dialogue**, not podcasts. To find a *podcast* by name, use [`GET /v1/podcasts?q=…`](/podcasts/overview). To resolve a podcast from an Apple / Spotify / YouTube identifier, use [`GET /v1/podcasts/lookup`](/podcasts/lookup). `GET /v1/podcasts/search` is a legacy alias for this endpoint. It still works, but it will eventually be repurposed for podcast (show) search — use `/v1/podcasts/episodes/search` in new integrations. ## When to use Search vs Mentions The Particle podcast surface ships two complementary search endpoints. Pick by what you're really asking. | You want… | Use this | | ----------------------------------------------------- | -------------------------------------------------------------------- | | Dialogue that *means* something — paraphrase tolerant | [`/v1/podcasts/episodes/search?semantic_search=…`](/podcasts/search) | | Dialogue containing exact tokens or phrases (BM25) | [`/v1/podcasts/episodes/search?keyword_search=…`](/podcasts/search) | | Both: ideas plus required terms | `/v1/podcasts/episodes/search?semantic_search=…&keyword_search=…` | | **Every line where a person or company is mentioned** | [`/v1/podcasts/mentions?entity_id=…`](/podcasts/mentions) | | Conceptual search scoped to a person/company | `/v1/podcasts/episodes/search?semantic_search=…&entity_id=…` | Search ranks segments by relevance. Mentions returns episodes with all of their mention windows in time order. Different shapes, different jobs. ## `semantic_search` — search by meaning Vector search is the right tool when the surface words in dialogue might not match the surface words in your query. Two speakers can discuss the same idea using totally different vocabulary, and a lexical engine misses both. Express your query the way you'd describe the topic to a colleague — full sentences are welcome. ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/episodes/search" \ --data-urlencode "semantic_search=hosts arguing about whether the Fed is staying too hawkish given how much core inflation has cooled" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` The above will surface segments that talk about *restrictive monetary policy*, *the FOMC's bias*, *PCE moderation*, or *real rates being too high* — even when the words "hawkish" or "Fed" never appear. What `semantic_search` is **not** good at: | Don't ask it… | Use this instead | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | "Every line about Sam Altman" — that's an entity question, not a content question. | [`/v1/podcasts/mentions?entity_id=sam-altman`](/podcasts/mentions) | | "Podcasts where Sam Altman is a guest" — structural metadata about an episode, not its dialogue. | [`/v1/podcasts/mentions?entity_id=sam-altman&role=guest`](/podcasts/mentions) | | `"OpenAI Q*"` — when a specific token must appear verbatim, BM25 is more reliable than vector similarity. | `keyword_search=OpenAI Q*` | | `"AGI AND not safety"` — boolean logic isn't supported. Express the underlying intent in natural language; combine with `keyword_search` if a literal term must also appear. | `semantic_search=…&keyword_search=…` | ## `keyword_search` — search by exact tokens (BM25) Use this when the exact form of a token matters: company tickers, drug names, model numbers, hashtags. Tokens are matched after the same normalization the index applies (lowercased, English tokenizer, no stemming). Multi-word queries are matched as a bag of tokens, ranked by BM25. ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/episodes/search" \ --data-urlencode "keyword_search=GLP-1" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Hybrid: `semantic_search` + `keyword_search` When you want segments that *express an idea* **and** *contain a specific term*, send both. Results are produced by running each leg independently and fusing them via reciprocal rank fusion. ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/episodes/search" \ --data-urlencode "semantic_search=existential concerns about AI systems acting outside human control" \ --data-urlencode "keyword_search=alignment" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Scoping a ranked search to an entity `entity_id` and `company_id` here are *filters* — they narrow ranked candidates to episodes featuring the resolved entity. The ranking still comes from `semantic_search` / `keyword_search`. To read every line about an entity, use [Mentions](/podcasts/mentions) instead. ```bash theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/episodes/search" \ --data-urlencode "semantic_search=AGI timelines and what plausible paths to it look like" \ --data-urlencode "entity_id=sam-altman" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Response ```json theme={"dark"} { "data": [ { "episode": { "id": "…", "title": "…", "published_at": "2026-…", "podcast": {"id": "…", "title": "All-In"} }, "segment": { "id": "…", "type": "TOPIC_DISCUSSION", "title": "…", "start_seconds": 1284, "end_seconds": 1620 }, "windows": [ { "start_seconds": 1305, "end_seconds": 1318, "lines": [ {"number": 217, "speaker": "Chamath Palihapitiya", "role": "HOST", "start_seconds": 1305, "end_seconds": 1310, "text": "…"}, {"number": 218, "speaker": "Jason Calacanis", "role": "HOST", "start_seconds": 1310, "end_seconds": 1314, "text": "…", "is_match": true}, {"number": 219, "speaker": "Chamath Palihapitiya", "role": "HOST", "start_seconds": 1314, "end_seconds": 1318, "text": "…"} ] } ], "clips": [ {"id": "…", "title": "…", "engagement_score": 78, "start_seconds": 1310.5, "end_seconds": 1382.2} ], "match": {"source": "semantic", "relevance_score": 0.82} } ], "has_more": true, "cursor": "r.AbCd…", "entity": {"id": "…", "slug": "sam-altman", "name": "Sam Altman"} } ``` `windows` is bounded — never the whole segment. Each window centers on one or more high-relevance lines (flagged `is_match: true`) padded with surrounding context. A single segment can produce multiple non-overlapping windows when the top-scored lines are far apart inside it. When the line-scoring path can't pinpoint a match (e.g., a degraded embedding service or no individual line scored above zero), the window falls back to the segment's opening lines and is flagged `is_preview: true`. `match.source` is `semantic`, `keyword`, or `hybrid` — branch on it when rendering. `clips` is omitted when no highlight clip overlaps the segment. The page-level `entity` block appears when an `entity_id` or `company_id` filter was provided **and resolved successfully**; if the reference can't be resolved the response comes back with an empty `data` array and `entity` omitted. ## Filters | Param | Notes | | -------------------------- | ---------------------------------------------------------------------------------------------- | | `podcast_id` | Slug, ID, or numeric iTunes ID. | | `episode_id` | Restrict to a single episode. | | `entity_id` / `company_id` | Filter (not the primary query). For "every line about X" use [Mentions](/podcasts/mentions). | | `role` | `guest`, `host`, `panelist`, `correspondent`, `mention`. Requires `entity_id` or `company_id`. | | `type` | Segment type filter (e.g. `INTERVIEW`). | | `since` / `until` | Episode `published_at` window. ISO date. | | `sort` | `relevance` (default) or `recency`. | ## Pagination Standard `limit` (1–100, default 25) + opaque `cursor`. Pass the `cursor` from the previous response back as `?cursor=…` to fetch the next page. Cursors are opaque — don't parse them. ## Related * [Mentions](/podcasts/mentions) — every line where a person or company is mentioned, episode-grouped. * [Transcripts → mentions in one episode](/podcasts/transcripts#transcript-mentions) — every entity mentioned in a single episode. * [Episodes](/podcasts/episodes) — episode-level recall when you don't need dialogue. # Segments & clips Source: https://docs.particle.pro/podcasts/segments-and-clips AI-identified structural sections of every episode and engagement-scored highlight clips. Two related but distinct breakdowns of every episode: * **Segments** are *structural* — contiguous sections classified by purpose (intro, ad, topic discussion, interview, transition). They cover 100% of the episode timeline and answer "what's happening, when?" * **Clips** are *highlight-driven* — short standalone moments scored for engagement and tagged with a type (`INSIGHTFUL`, `FUNNY`, `CONTROVERSIAL`, `AHA_MOMENT`, …). Typically a handful per episode. They answer "what's worth sharing?" For MCP agents, a known episode's clips are bundled into [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) with `include: ["clips"]` (and segments with `include: ["segments"]`). Clips that overlap a dialogue match arrive inline on [`particle_podcast_search_transcripts`](/mcp/tools/podcasts/podcast-search-transcripts). ## Segments ### Segment types | Type | What it captures | | ------------------ | ----------------------------------------------------- | | `INTRO` | Opening of the episode | | `OUTRO` | Closing | | `TOPIC_DISCUSSION` | In-depth discussion of a specific subject | | `INTERVIEW` | Question-and-answer with a guest | | `PERSONAL_BANTER` | Off-topic or personal conversation | | `TRANSITION` | Bridge between segments | | `AD` | Advertisement, sponsorship, or cross-promotional read | ### Segments for an episode In chronological order, this is the structural outline of the episode. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/segments" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "number": 1, "type": "AD", "title": "Sponsor: The Build Podcast", "start_seconds": 0.56, "end_seconds": 28.94 }, { "number": 4, "type": "INTRO", "title": "Show Open and Banter", "start_seconds": 102.94, "end_seconds": 242.4 }, { "number": 5, "type": "TOPIC_DISCUSSION", "title": "WH Correspondents Dinner Shooting and Media Coverage", "start_seconds": 242.46, "end_seconds": 1233.69 }, { "number": 9, "type": "TOPIC_DISCUSSION", "title": "Musk v. Altman Trial and AI Industry Moves", "start_seconds": 1435.49, "end_seconds": 2228.04 }, { "number": 21, "type": "OUTRO", "title": "Show Close and Credits", "start_seconds": 4448.06, "end_seconds": 4502.34 } // 21 segments returned; abbreviated for readability ], "has_more": false, "cursor": null } ``` Each segment has its own [transcript endpoint](/podcasts/transcripts#segment-and-clip-transcripts) and an audio URL with the extracted MP3. To filter by type server-side (for example, to skip ads), use the cross-catalog endpoint with an episode filter: `GET /v1/podcasts/segments?episode_id={id}&type=AD`. ### Cross-episode segment lookup ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/segments?type=INTERVIEW&limit=10" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` At least one of `episode_id`, `podcast_id`, or `type` is required — the endpoint does not return a global feed. Combine filters to narrow further (e.g., `?podcast_id=all-in&type=INTERVIEW`). ## Clips Clips are the highlight moments — the parts of an episode worth pulling out for sharing, embedding, or social posts. Each is AI-extracted, scored for engagement, and tagged with a type. ### Clip types | Category | Types | | ------------------- | ------------------------------------------------------------------------- | | Takes & opinions | `SPICY`, `CONTROVERSIAL`, `DEBATE_DISAGREEMENT` | | Emotion | `FUNNY`, `EMOTIONAL`, `SHOCKING` | | Insight & knowledge | `INSIGHTFUL`, `INFORMATIVE`, `EDUCATIONAL`, `PHILOSOPHICAL`, `AHA_MOMENT` | | Highlights | `NOTABLE_LINE`, `BEST_STORY` | ### Clips for an episode Returned ranked by engagement score (highest first): ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/clips?limit=2" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "7ULKq1QRmEKqICZJKa6orv", "type": "INSIGHTFUL", "title": "Targets, fame, and gun access", "intro_statement": "First, what truly drives these attacks?", "engagement_score": 84, "start_seconds": 356.3, "end_seconds": 414.96, "duration_seconds": 58.66, "audio_url": "https://cdn.particle.pro/podcast/episode/…/clip/audio/….mp3", "speaker": { "name": "Scott Galloway", "role": "HOST", "entity_slug": "scott-galloway" }, "segment": { "id": "68yiT3ivEGRsDjMgWjO9rT", "type": "TOPIC_DISCUSSION", "title": "WH Correspondents Dinner Shooting and Media Coverage" } } // … ] } ``` Each clip carries: * `audio_url` — direct MP3 of the clip; embed it without slicing yourself * `intro_statement` — ready-to-use share copy * `engagement_score` — integer 0–100. Higher is more shareable; values above 70 are typical for a strong clip. * `speaker` — who's speaking, with a knowledge-graph link * `segment` — which structural section the clip came from ### Discovering clips by what's discussed Highlight clips are returned alongside their parent segment by `GET /v1/podcasts/episodes/search`. Whenever a clip's time range overlaps the matching segment, it appears in the result's `clips` array. ```bash curl theme={"dark"} # Search the dialogue by meaning; clips on a matching segment come along. curl --get "https://api.particle.pro/v1/podcasts/episodes/search" \ --data-urlencode "semantic_search=panel debating whether superintelligent AI will be controllable" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```bash curl theme={"dark"} # Every line about a person inside a single podcast — use /mentions for # entity-mention search; /search rejects requests without semantic_search # or keyword_search. curl --get "https://api.particle.pro/v1/podcasts/mentions" \ --data-urlencode "entity_id=sam-altman" \ --data-urlencode "podcast_id=pivot" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` See the [search docs](/podcasts/search) for ranked dialogue search, and the [mentions docs](/podcasts/mentions) for entity-mention search. `GET /v1/podcasts/clips` (above) is for *browsing* highlight clips ranked by engagement. For *text-based* discovery — by topic or keyword — use [`/v1/podcasts/episodes/search`](/podcasts/search) (clips on matching segments come along). For entity-mention discovery, use [`/v1/podcasts/mentions`](/podcasts/mentions). ## Embeds Public clip embed endpoints (no API key required) return shareable JSON suitable for iframe-style players: ```bash theme={"dark"} curl "https://api.particle.pro/v1/embed/clips/7ULKq1QRmEKqICZJKa6orv" ``` The embed payload includes the clip title, intro statement, audio URL, podcast artwork, and duration — everything you need for a thumbnail player. ## Related * [Transcripts](/podcasts/transcripts) — fetch dialogue scoped to a segment or clip * [Episodes](/podcasts/episodes) — list and filter episodes * [Concepts](/concepts) — pagination, slugs, pricing weight # Episode stream (firehose) Source: https://docs.particle.pro/podcasts/stream Real-time Server-Sent Events firehose that pushes podcast episodes to you as they are ingested. The episode stream is a long-lived [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/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. 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. ``` GET https://api.particle.pro/v1/podcasts/episodes/stream POST https://api.particle.pro/v1/podcasts/episodes/stream ``` Use **GET** for no filter, a popularity threshold, or a small explicit set of podcasts. Use **POST** (with a JSON body) when you want to filter on a large `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. | 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. ## 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 an `error` event 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), so `0.9` ≈ the top 10% most popular podcasts. Use this to follow "the popular stuff" without enumerating ids. Pass a large `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](#resuming-after-a-disconnect). | | `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](#hydrate-the-payload). | ## Open the stream A simple GET — all transcribed episodes, live: ```bash curl theme={"dark"} # -N disables curl's output buffering so events print as they arrive. curl -N "https://api.particle.pro/v1/podcasts/episodes/stream?milestone=transcribed" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```python Python theme={"dark"} with httpx.stream( "GET", "https://api.particle.pro/v1/podcasts/episodes/stream", params={"milestone": "transcribed"}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, timeout=None, ) as res: for line in res.iter_lines(): ... # parse SSE events ``` Filtered by popularity, or by a handful of shows (GET): ```bash theme={"dark"} # Top ~10% most popular podcasts, at the segmented milestone: curl -N "https://api.particle.pro/v1/podcasts/episodes/stream?milestone=segmented&popularity_threshold=0.9" \ -H "X-API-Key: $PARTICLE_API_KEY" # A few specific shows by slug: curl -N "https://api.particle.pro/v1/podcasts/episodes/stream?podcast_ids=pivot,lex-fridman,all-in" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` A large explicit set (POST with a JSON body): ```bash theme={"dark"} curl -N -X POST "https://api.particle.pro/v1/podcasts/episodes/stream?milestone=transcribed" \ -H "X-API-Key: $PARTICLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "podcast_ids": ["pivot", "lex-fridman", "QpMz7GYKfSNuUa6zKXA4Q", "... up to 1000 ..."] }' ``` With no `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: ```jsonc theme={"dark"} event: episode data: { "milestone": "transcribed", "cursor": "g3Qk9m8...", // opaque resume token — store this "episode": { "id": "78cgekLUjCJBUZbj3s5K8Y", "title": "WHCD Shooting Aftermath, Musk and Altman Face-Off…", "podcast": { "id": "QpMz7GYKfSNuUa6zKXA4Q", "title": "Pivot" }, "published_at": "2026-04-28T10:00:00Z", "has_transcript": true // …same shape as a /v1/podcasts/episodes list entry } } ``` The `episode` object is the same list-shaped representation returned by [list episodes](/podcasts/episodes) and the [feed](/podcasts/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`](#hydrate-the-payload). **`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: ```jsonc theme={"dark"} event: error data: { "message": "no podcasts matched the provided filter (podcast_ids / popularity_threshold)" } ``` ## 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 | — | 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). ```bash theme={"dark"} # Segments at the segmented milestone, embedded in each event: curl -N "https://api.particle.pro/v1/podcasts/episodes/stream?milestone=segmented&include=segments" \ -H "X-API-Key: $PARTICLE_API_KEY" # Everything available at the terminal milestone: curl -N "https://api.particle.pro/v1/podcasts/episodes/stream?milestone=fully_ingested&include=all" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Word-level transcripts are paginated and can't be embedded inline; fetch them from `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.** **Disconnections are normal — design for automatic reconnection from day one.** A long-lived stream **will** be interrupted periodically, and most often it's not your network: we ship frequently, and every deploy does a **rolling restart** of the serving pods, which closes all open streams. Idle proxies and load balancers also recycle long connections. This is routine operation, **not an error and not data loss** — the durable log + your cursor guarantee a gap-free resume. Treat "the connection ended" as an ordinary, expected event your client handles silently, not an exception to alert on. Build the reconnect-with-backoff loop in from your very first implementation (see [A resilient consumer](#a-resilient-consumer)); a client that assumes one connection stays open indefinitely will break during the next deploy. ### The cursor Every `episode` 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 on `episode.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=`: ``` GET /v1/podcasts/episodes/stream?milestone=transcribed&cursor=g3Qk9m8... ``` The stream first **replays** every episode after that cursor (catch-up), then transitions seamlessly to live. (On POST, send the same body and the updated `?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 each `episode` 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. ```js JavaScript theme={"dark"} let cursor = await loadSavedCursor(); // null on first run const seen = new Set(); let backoff = 1000; // ms; grows on repeated failure, resets on success const MAX_BACKOFF = 30000; while (true) { const url = new URL("https://api.particle.pro/v1/podcasts/episodes/stream"); url.searchParams.set("milestone", "transcribed"); if (cursor) url.searchParams.set("cursor", cursor); try { const res = await fetch(url, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); for await (const evt of parseSSE(res.body)) { if (evt.event === "error") break; // terminal; reconnect from `cursor` if (evt.event !== "episode") continue; const { episode, cursor: next } = JSON.parse(evt.data); if (!seen.has(episode.id)) { seen.add(episode.id); await handleEpisode(episode); // idempotent } cursor = next; // advance only after successful processing await saveCursor(cursor); backoff = 1000; // healthy connection — reset backoff } } catch (err) { // network error / stream closed (e.g. a deploy) — fall through to reconnect } // Exponential backoff with jitter, capped. The disconnect itself is expected; // this just avoids reconnect storms during a longer outage. const delay = Math.min(backoff, MAX_BACKOFF) * (0.5 + Math.random() / 2); await sleep(delay); backoff = Math.min(backoff * 2, MAX_BACKOFF); } ``` `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](/podcasts/feed) is the all-plans pull alternative — the same episodes, milestones, and filters, returned by a resumable `GET` 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](/podcasts/episodes) (which also accepts `fully_ingested=true`) is simpler still. ## Related * [Episodes](/podcasts/episodes) — the same episode shape, by query or by ID * [Transcripts](/podcasts/transcripts) — dialogue available once an episode reaches `transcribed` * [Segments & clips](/podcasts/segments-and-clips) — available at `segmented` and `fully_ingested` # Brand Suitability Source: https://docs.particle.pro/podcasts/suitability Industry-standard content risk and advertiser-fit assessment for podcasts: per-category prevalence and treatment, evidence excerpts, and a deterministic trend across analyses. Particle API assesses every well-classified podcast against the 12 categories from the [IAB Tech Lab Content Taxonomy 3.x Brand Safety & Suitability Framework](https://iabtechlab.com/standards/content-taxonomy/) — the industry-standard taxonomy that advertisers, agencies, DSPs, and ad-verification vendors (IAS, DoubleVerify, Spotify, Hive, Sounder, Barometer) train against. The framework was originally published by GARM (Global Alliance for Responsible Media) in 2022; IAB Tech Lab assumed stewardship after GARM was discontinued in 2024 and continues to publish and version it. Each assessment covers a contiguous, non-overlapping window of recent episodes, refreshed on a risk-tiered cadence. Available to MCP agents through [`particle_podcast_resolve`](/mcp/tools/podcasts/podcast-resolve): every result carries the high-level `suitability_tier` enum unconditionally, and passing `include: ["suitability"]` attaches the per-category breakdown alongside the podcast. Corpus-wide publisher views are [`particle_podcast_get_suitability_leaderboard`](/mcp/tools/podcast_suitability/podcast-get-suitability-leaderboard) and [`particle_podcast_list_suitability_category_publishers`](/mcp/tools/podcast_suitability/podcast-list-suitability-category-publishers) (opt-in `podcast_suitability` category). ## What the assessment gives you A buy-or-skip decision needs more than a flat 0–1 risk score per category. A true-crime documentary about Bundy is heavy on crime content but advertiser-friendly; a host endorsing violent vigilantism is light on crime content but advertiser-toxic. The Particle assessment captures that distinction along five dimensions: * **Per-category prevalence** — `NONE`, `INCIDENTAL`, `OCCASIONAL`, `FREQUENT`, `PERVASIVE`. How often the category appears across the analyzed window. * **Per-category treatment** — `ABSENT`, `DOCUMENTARY`, `EDITORIAL`, `PROMOTIONAL`, `GLAMORIZING`. How the content is framed when it appears. Treatment is the entire reason a news show covering terrorism stays advertiser-friendly while a podcast endorsing it does not. * **Per-category risk level** — derived deterministically from `(prevalence, treatment, code)`. `NONE`, `LOW`, `MEDIUM`, `HIGH`, or `FLOOR` (violates the Brand Safety Floor). * **Per-category evidence** — every category with prevalence above `NONE` carries one or more cited episode excerpts so you can audit the verdict and defend planner decisions to clients. * **Methodology transparency** — sample size, confidence, and the time window of episodes that produced the verdict, all on every response. ## Tiers The overall verdict maps to the framework's four risk levels: | Tier | Framework risk level | Advertiser fit | | ----------- | -------------------- | ------------------------------------------------------------------------------------ | | `SAFE` | Low Risk | Suitable for all advertisers, including kid-directed and family brands. | | `LIMITED` | Medium Risk | Suitable for most advertisers; family brands may apply caution. | | `SENSITIVE` | High Risk | Many brands skip; suitable for general-audience and adult-product advertisers. | | `UNSAFE` | Brand Safety Floor | No monetization — content violates the Brand Safety Floor for at least one category. | ## Categories All 12 brand-safety categories are evaluated on every assessment, even when prevalence is `NONE`, so the response records that the category was actively considered. | Code | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `adult_sexual` | Adult or sexually explicit content. | | `arms_ammunition` | Arms, ammunition, and weapon-related content. | | `crime_harmful_acts` | Crime, harmful acts to individuals, and human-rights violations. | | `death_injury_military_conflict` | Death, injury, or military conflict. | | `online_piracy` | Online piracy and copyright infringement. | | `hate_speech_aggression` | Hate speech and acts of aggression directed at protected groups. | | `obscenity_profanity` | Obscenity and profanity, including gestures. | | `illegal_drugs_alcohol_tobacco` | Illegal drugs, tobacco, e-cigarettes, vaping, or alcohol. | | `spam_harmful` | Spam or harmful content (malware, phishing, scams). | | `terrorism` | Terrorism, including its promotion or glorification. | | `debated_social_issues` | Debated sensitive social issues with insensitive or harmful treatment. | | `misinformation` | Factually incorrect or deliberately misleading content presented as fact (added in the September 2023 framework revision). | ## Get a podcast's suitability ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-jane-doe-show/suitability" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/the-jane-doe-show/suitability", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const assessment = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/the-jane-doe-show/suitability", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) assessment = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "overall_tier": "LIMITED", "confidence": "HIGH", "summary": "True-crime show with documentary treatment of historical American cases. No glorification; sponsor base is mainstream consumer brands.", "methodology": "Reviewed segments for 12 episodes from a 6-month window; read full transcripts for 5. Cross-checked sponsors for drug- and predatory-finance categories.", "episodes_analyzed": 25, "sample_window_start_at": "2025-11-04T00:00:00Z", "sample_window_end_at": "2026-04-29T00:00:00Z", "evaluated_at": "2026-05-02T18:14:21Z", "categories": [ { "code": "crime_harmful_acts", "description": "Crime, harmful acts to individuals, and human-rights violations.", "prevalence": "FREQUENT", "treatment": "DOCUMENTARY", "risk_level": "MEDIUM", "reasoning": "Hosts narrate established cases without endorsement, citing primary sources and court records.", "evidence": [ { "episode_id": "78cgekLUjCJBUZbj3s5K8Y", "episode_title": "The Jane Doe Case", "episode_slug": "the-jane-doe-case", "excerpt": "We're going to walk through the timeline based on the trial transcripts..." } ] }, { "code": "obscenity_profanity", "description": "Obscenity and profanity, including gestures.", "prevalence": "OCCASIONAL", "treatment": "EDITORIAL", "risk_level": "LOW", "reasoning": "Profanity used for emphasis, never directed at protected groups.", "evidence": [/* … */] } // 10 more entries — every brand-safety category appears, even when prevalence=NONE. ] } ``` ## Optional sections Pass `include=trend`, `include=history`, or `include=trend,history` to expand the response. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/the-jane-doe-show/suitability?include=trend,history" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` `trend` is computed deterministically from the diff between the two most recent assessments — the agent never compares across windows. Each analysis covers a contiguous, non-overlapping window of episodes, so the comparison is genuinely "what was true in the prior window vs. what's true now." ```jsonc Response (truncated) theme={"dark"} { "overall_tier": "LIMITED", // ... main body unchanged ... "trend": { "direction": "STABLE", "prior_evaluated_at": "2025-11-12T03:55:08Z", "changed_categories": [ { "code": "obscenity_profanity", "prior_prevalence": "INCIDENTAL", "new_prevalence": "OCCASIONAL", "prior_treatment": "EDITORIAL", "new_treatment": "EDITORIAL", "prior_risk_level": "LOW", "new_risk_level": "LOW" } ] }, "history": [ { "overall_tier": "LIMITED", "confidence": "HIGH", "episodes_analyzed": 25, "sample_window_start_at": "2025-05-01T00:00:00Z", "sample_window_end_at": "2025-11-04T00:00:00Z", "evaluated_at": "2025-11-12T03:55:08Z" } // … ] } ``` `direction` is one of `STABLE`, `IMPROVING`, `DECLINING`, or `INSUFFICIENT_HISTORY` (when fewer than two assessments exist). ## Filter the catalog by tier `GET /v1/podcasts` accepts a `suitability_tier` query parameter. Returns only podcasts whose most recent assessment landed at that tier — useful for buyer-side catalog scans: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts?suitability_tier=SAFE&limit=20" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` The high-level `suitability_tier` enum is also surfaced on every podcast object in list and detail responses; the per-category breakdown, evidence, and methodology are gated to the dedicated suitability endpoint. ## Refresh cadence Each assessment covers a contiguous, non-overlapping window of recent episodes — the most-recent \~25, scoped above the prior assessment's window so no episode is ever re-analyzed across runs. Refresh thresholds depend on the prior tier: | Prior tier | Re-evaluates after this many new episodes | Max wall-clock staleness | | ----------- | ----------------------------------------- | ------------------------ | | `SAFE` | 25 new episodes since last window | 24 months | | `LIMITED` | 20 new episodes | 24 months | | `SENSITIVE` | 15 new episodes | 12 months | | `UNSAFE` | 10 new episodes | 9 months | A daily show producing \~30 episodes per month therefore refreshes roughly monthly when SAFE and weekly when UNSAFE. A weekly show refreshes roughly every 5–6 months when SAFE. ## Limitations * **Minimum coverage.** A first assessment requires at least 5 episodes with classified topics. Podcasts below this threshold return a 404 from this endpoint. * **English-language bias.** Treatment classification is most reliable for English-language content; languages with limited transcript coverage may produce lower-confidence assessments. * **Sample-based.** Each assessment is a snapshot of a contiguous window, not the full catalog. Across many assessments over a podcast's lifetime, the union of windows covers the publishing history; for catalog-wide claims combine `include=history` with the per-window `sample_window_start_at` / `sample_window_end_at` boundaries. ## Related * [Bias analysis](/podcasts/overview#bias-analysis) — political bias rating, the closest analog (per-podcast LLM-derived assessment with evidence and methodology). * [Episodes](/podcasts/episodes) — drill into the episodes that produced an assessment. * [Concepts → Pricing weight](/concepts#pricing-weight) — suitability calls are priced higher per call. # Transcripts Source: https://docs.particle.pro/podcasts/transcripts Diarized dialogue, word-level timestamps, entity mentions in context, and SRT export. Every transcribed episode has three transcript views, each tuned for a different use case: * **Dialogue** — speaker-attributed lines with sentence-level timestamps. The default. Best for chat-style UIs, LLM context windows, and most reading. * **Words** — every word individually timestamped. Best for karaoke-style highlighting, precision audio editing, and word-aligned search. * **Mentions** — lines surrounding mentions of a specific entity, with `is_mention` flags. Best for "what did they say about X?" workflows. Dialogue transcripts are also reachable scoped to a single segment or clip. Available to MCP agents via [`particle_podcast_get_episode`](/mcp/tools/podcasts/podcast-get-episode) with `include: ["transcript"]` (and optional `transcript_format`, `transcript_speaker`, `transcript_start`, `transcript_end`). For mention-style windows, use [`particle_podcast_find_mentions`](/mcp/tools/podcasts/podcast-find-mentions). ## Dialogue transcript ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/transcript" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/transcript", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { lines } = await res.json(); ``` ```jsonc Response (truncated) theme={"dark"} { "episode_id": "78cgekLUjCJBUZbj3s5K8Y", "language": "en", "duration_seconds": 4206, "lines": [ { "number": 1, "speaker": "Scott Galloway", "role": "CO_HOST", "start_seconds": 0.56, "end_seconds": 7.74, "text": "This episode is brought to you by The Build Podcast…" } // … ] } ``` Lines are ordered, 1-indexed, and include speaker name and role. Note that the first lines of many episodes are sponsorship reads — fetch `/segments` to see which time ranges are tagged `AD` if you want to skip them. See [segments](/podcasts/segments-and-clips#segments). ## Output formats Use the `format` query parameter: Structured JSON with speaker attribution, roles, and timestamps per line. ```bash theme={"dark"} curl ".../transcript?format=dialogue" ``` Best for: building conversation UIs, speaker analysis, programmatic processing. `Speaker: text` lines, one per turn. ```bash theme={"dark"} curl ".../transcript?format=text" ``` Best for: LLM context windows, full-text display, search indexing. Standard SubRip format with speaker labels. ```bash theme={"dark"} curl ".../transcript?format=srt" ``` Best for: video players, caption rendering, accessibility tools. ## Filter by speaker or time range Extract everything one person said: ```bash theme={"dark"} curl ".../transcript?speaker=Kara+Swisher" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Extract a time range: ```bash theme={"dark"} # Minute 5 to minute 15 curl ".../transcript?start=300&end=900" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Combine — exactly what one person said during one segment: ```bash theme={"dark"} curl ".../transcript?speaker=Scott+Galloway&start=1435&end=2228&format=text" ``` ## Word-level transcript For per-word timing — karaoke-style highlighting, precision editing, word-aligned search: ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/transcript/words?start=0&end=10" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "episode_id": "78cgekLUjCJBUZbj3s5K8Y", "language": "en", "words": [ { "text": "This", "type": "word", "start_seconds": 0.56, "end_seconds": 0.74, "speaker": "Scott Galloway" }, { "text": " ", "type": "spacing", "start_seconds": 0.74, "end_seconds": 0.82, "speaker": "Scott Galloway" }, { "text": "episode", "type": "word", "start_seconds": 0.82, "end_seconds": 1.12, "speaker": "Scott Galloway" } // … ], "has_more": false } ``` Word-level transcripts for long episodes can be very large — a feature-length episode runs to tens of thousands of entries. Pass `start` and `end` to clip a time range, `limit` to bound the page size, and `exclude_spacing=true` if you don't need the inter-word whitespace tokens. ### Query parameters | Param | Default | Description | | ----------------- | ------------------ | ---------------------------------------------------------------- | | `start` | `0` | Start time in seconds for time-range clipping. | | `end` | end of episode | End time in seconds for time-range clipping. | | `limit` | unset (return all) | Max words per page (1–5000). Omit to return every matching word. | | `cursor` | — | Opaque cursor from a previous `cursor` response field. | | `exclude_spacing` | `false` | Drop `type:"spacing"` tokens. See below. | ### Spacing tokens Roughly half of the entries in a typical word transcript have `type:"spacing"` and `text:" "` — these represent the silence *between* spoken words, with their own `start_seconds`/`end_seconds`. They're useful when timing matters: caption/subtitle UIs that need precise pause durations, karaoke-style word highlighting against playback, or reconstructing inter-word silences for audio alignment. NLP, search, and LLM consumers should pass `exclude_spacing=true` to skip them — it roughly halves the payload. ### Pagination When you pass `limit`, the response includes a `cursor` and `has_more: true` until the last page: ```bash theme={"dark"} # First page curl ".../transcript/words?limit=1000&exclude_spacing=true" \ -H "X-API-Key: $PARTICLE_API_KEY" # Next page — paste the cursor from the prior response curl ".../transcript/words?limit=1000&exclude_spacing=true&cursor=r.AbCdEf" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` The `speaker` field is the identified speaker name (e.g. `Scott Galloway`). When a speaker hasn't been resolved to a name, the raw STT label (`speaker_0`, `speaker_1`, …) is returned instead. ## Transcript mentions The most useful transcript view when you care about a specific person, company, or topic. Returns the dialogue lines around every mention of an entity in one episode, with `is_mention: true` on the lines that actually contain the mention and surrounding lines for context. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/transcript/mentions?entity_id=sam-altman" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```jsonc Response (truncated) theme={"dark"} { "episode_id": "78cgekLUjCJBUZbj3s5K8Y", "entities": [ { "entity": { "id": "5MBAHcKUujL2dzPXrgfQ8E", "slug": "sam-altman", "name": "Sam Altman" }, "total_mention_count": 5, "mention_variants": ["Sam Altman"], "mentions": [ { "lines": [ { "number": 363, "speaker": "Kara Swisher", "text": "Uh, let's go on a quick break.", "is_mention": false, "start_seconds": 1232.53, "end_seconds": 1233.69 }, { "number": 364, "speaker": "Kara Swisher", "text": "When we come back, Elon Musk and Sam Altman head to court.", "is_mention": true, "start_seconds": 1233.79, "end_seconds": 1236.51 }, { "number": 365, "speaker": "Kara Swisher", "text": "Big story, actually.", "is_mention": false, "start_seconds": 1236.65, "end_seconds": 1238.09 } ], "start_seconds": 1231.47, "end_seconds": 1246.56 } // … ] } ], "has_more": true, "cursor": "r.AQAAABk" } ``` Use `start_seconds` / `end_seconds` to deep-link into the audio, or feed the line text into an LLM with full speaker context. ### Chunking semantics The endpoint groups transcript lines into context **windows**: * `context_lines` (default `2`, max `20`) is the radius of each window. A mention on line `i` produces the closed range `[i - context_lines, i + context_lines]`, clamped at the transcript boundaries — so a single mention yields up to `2 * context_lines + 1` lines. * Adjacent or overlapping windows **merge** into a single window. Two mentions within `context_lines` of each other produce one entry with multiple `is_mention: true` lines, not two entries. * `total_mention_count` is the unfiltered count of dialogue lines containing at least one mention of the entity in the episode. It is independent of pagination, and equals the sum of `is_mention=true` flags across the un-paginated `mentions[]`. * Mention matching is a case-sensitive substring match against `mention_variants` (the entity's canonical name plus any annotated aliases). ### Pagination | Caller shape | Pagination axis | Cursor refers to | | -------------------- | ---------------------- | ----------------------------------- | | `entity_id` provided | `mentions[]` (windows) | offset into windows for that entity | | `entity_id` omitted | `entities[]` | offset into entity list | When `entity_id` is set, exactly one entity is returned and its `mentions[]` is paged. When `entity_id` is omitted, each entity in the page returns **all** of its mentions; deep-paging through a single entity's mentions requires re-issuing with that `entity_id`. `limit` defaults to `25` and is capped at `100`. Pass the `cursor` from a previous response to fetch the next page; `has_more` indicates whether more results exist. ```bash theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/78cgekLUjCJBUZbj3s5K8Y/transcript/mentions?entity_id=sam-altman&limit=25&cursor=r.AQAAABk" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ## Segment and clip transcripts Segments and clips have their own transcript endpoints scoped to that time range: ```bash theme={"dark"} # Segment transcript curl "https://api.particle.pro/v1/podcasts/segments/{segment_id}/transcript" \ -H "X-API-Key: $PARTICLE_API_KEY" # Clip transcript with SRT export curl "https://api.particle.pro/v1/podcasts/clips/{clip_id}/transcript?format=srt" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` Both accept the `format` query parameter (`dialogue`, `text`, or `srt`). ## Related * [Episodes](/podcasts/episodes) — discovery and sub-resources * [Segments & clips](/podcasts/segments-and-clips) — get a clip ID, then its transcript * [Knowledge graph → entities](/knowledge-graph/entities) — pick the entity to mention-search # Quickstart Source: https://docs.particle.pro/quickstart Trace one person across podcasts, dialogue, and companies in five minutes. In the next five minutes you'll find a person in the knowledge graph, read every line of dialogue where they're discussed across the podcast catalog, find the episodes where they were actually a guest, pull the highlight clips, search the catalog by topic, then pivot to the companies they're connected to and the podcasts those companies sponsor — all with real, current data. Every example below was run against `https://api.particle.pro` while writing this page. Paste them with your own key and you'll see the same kind of results. ## Get an API key 1. Sign up or log in at the API Dashboard 2. Create an organization and project 3. Open the project's **API Keys** section 4. Click **Create API Key** and copy the key — it won't be shown again Set it as an environment variable so the rest of this guide reads cleanly: ```bash theme={"dark"} export PARTICLE_API_KEY="pk_…" ``` All endpoints accept the key in the `X-API-Key` header. See [API reference → Introduction](/api-reference/introduction) for the alternate `Authorization: Bearer` form. ## 1. Find a person Start with a name search. `/v1/entities?q=…` matches against the knowledge graph and returns ranked results with a slug for each — the slug is the handle you'll pass to every other entity-aware endpoint. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/entities?q=altman&limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/entities?q=altman&limit=3", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} import os, httpx res = httpx.get( "https://api.particle.pro/v1/entities", params={"q": "altman", "limit": 3}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response theme={"dark"} { "data": [ { "id": "17PzxG1t12xzno", "slug": "sam-altman", "name": "Sam Altman", "description": "CEO of OpenAI", "wikipedia_url": "https://en.wikipedia.org/wiki/Sam_Altman", "image_url": "https://cdn.particle.pro/url/media/kge/m/02kx06l/…" }, { "id": "44RuOByIGYS", "slug": "robert-altman", "name": "Robert Altman", "description": "American filmmaker", "wikipedia_url": "https://en.wikipedia.org/wiki/Robert_Altman" } // … ], "has_more": true, "cursor": "…" } ``` The first result is the one we want — `sam-altman`. Once you know an entity's slug, you can also fetch it directly with `GET /v1/entities/sam-altman` to get the full record. Every endpoint that takes an `{id}` accepts the canonical ID *or* the slug — pick whichever you have. We'll use `sam-altman` for the rest of the tutorial. ## 2. Read every line said about them Jump straight from "this is the entity" to "here is every line of dialogue across the podcast catalog where they're mentioned" — grouped by episode, recency-ordered, with the surrounding lines for context and the matched line flagged. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/mentions?entity_id=sam-altman&limit=2" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/mentions?entity_id=sam-altman&limit=2", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/mentions", params={"entity_id": "sam-altman", "limit": 2}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "episode": { "id": "1Se2PVIwRNIhRIcv5FgAPX", "title": "Evening Wire: Comey Indicted Again & Fauci Advisor Charged in COVID Cover-Up | 4.28.26", "published_at": "2026-04-28T21:30:00Z", "podcast": { "title": "Morning Wire", "slug": "morning-wire" } }, "mention_count": 1, "mention_variants": ["Sam Altman"], "windows": [ { "segment": { "type": "TOPIC_DISCUSSION", "title": "OpenAI Growth Miss and IPO Concerns" }, "start_seconds": 436.05, "end_seconds": 470.41, "lines": [ { "speaker": "Andy Valdez", "role": "CORRESPONDENT", "start_seconds": 452.77, "end_seconds": 460.21, "text": "As CEO Sam Altman continues to seek an IPO at an aggressive pace, slowing growth may turn away potential investors.", "is_mention": true } // …surrounding lines for context ] } ] } // … ], "has_more": true, "cursor": "s.…", "entity": { "slug": "sam-altman", "name": "Sam Altman", "description": "CEO of OpenAI" } } ``` Each episode group returns one or more `windows` — contiguous ranges of dialogue where the entity is mentioned, with `context_lines` of surrounding speech (default `2`, configurable up to `20`). Every line that actually names the entity has `is_mention: true`. The `start_seconds` and `end_seconds` deep-link into the audio. Use `/v1/podcasts/mentions` for "every line about an entity." Use [`/v1/podcasts/episodes/search`](/podcasts/search) for "every segment that *talks about* a topic" (paraphrase-tolerant) or contains a specific phrase (BM25). They're complementary — see [Search](/podcasts/search) for when to reach for which. The same endpoint answers "what has *one show* been saying about *one entity*?" Stack `entity_id` with `podcast_id` to scope dialogue to a single podcast — for example, every line the All-In podcast has aired about OpenAI: ```bash curl theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/mentions" \ --data-urlencode "entity_id=openai" \ --data-urlencode "podcast_id=all-in" \ --data-urlencode "limit=2" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const url = new URL("https://api.particle.pro/v1/podcasts/mentions"); url.searchParams.set("entity_id", "openai"); url.searchParams.set("podcast_id", "all-in"); url.searchParams.set("limit", "2"); const res = await fetch(url, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/mentions", params={"entity_id": "openai", "podcast_id": "all-in", "limit": 2}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "episode": { "id": "4KnhQ9DsSIxBhDN3BP5iFN", "title": "OpenAI Misses Targets, Codex vs Claude, Elon vs Sam Trial, Big Hyperscaler Beats, Peptide Craze", "published_at": "2026-05-01T21:37:00Z", "podcast": { "title": "All-In with Chamath, Jason, Sacks & Friedberg", "slug": "all-in" } }, "mention_count": 35, "mention_variants": ["OpenAI"], "windows": [ { "segment": { "type": "TOPIC_DISCUSSION", "title": "OpenAI misses targets and IPO debate" }, "start_seconds": 222.42, "end_seconds": 275.7, "lines": [ { "speaker": "Jason Calacanis", "role": "HOST", "start_seconds": 229.76, "end_seconds": 234.92, "text": "OpenAI has $600 billion in spending commitments for compute.", "is_mention": true } // …surrounding lines for context ] } // …more windows in this episode ] } // …more All-In episodes ], "has_more": true, "cursor": "s.1hwwoMp2ojPqMXPMC40Qn5lyYu2z9jHwDAbO", "entity": { "slug": "openai", "name": "OpenAI", "description": "Artificial intelligence company" } } ``` Same shape as the Sam Altman call — episodes, windows, mention-flagged lines — just narrowed to one show. Swap `podcast_id` for any other slug (`acquired`, `lex-fridman`, `the-tim-ferriss-show`) to ask the same question of a different audience. ## 3. Find episodes where they were actually a guest Mentions tells you where someone was *discussed*. Episodes tells you where they were *featured*. Filter by `entity_id` plus a `role` to get just the episodes where Sam Altman was on the show as a guest — not the hundreds of episodes where his name comes up in passing. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes?entity_id=sam-altman&role=guest&limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/podcasts/episodes?entity_id=sam-altman&role=guest&limit=3", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/episodes", params={"entity_id": "sam-altman", "role": "guest", "limit": 3}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "2cgrh56ehQYBlXwqecmZ9P", "title": "OpenAI CEO Sam Altman & Amazon CEO Andy Jassy 2/27/26", "podcast": { "title": "Squawk Pod", "slug": "squawk-pod" }, "published_at": "2026-02-27T17:08:52Z", "duration_seconds": 2916, "speakers": [ { "name": "Joe Kernen", "role": "HOST" }, { "name": "Sam Altman", "role": "GUEST", "entity": { "slug": "sam-altman" } }, { "name": "Andy Jassy", "role": "GUEST", "entity": { "slug": "andy-jassy" } } // … ], "segment_count": 17, "clip_count": 6, "has_transcript": true } // … ], "has_more": true, "cursor": "…" } ``` `role` accepts `guest`, `host`, `panelist`, `correspondent`, or `mention`. Drop the filter and you'll get every episode that touches the entity in any way — useful for broad recall, but for "where was Sam Altman actually interviewed" you want `role=guest`. Hold onto the first result's `id` (`2cgrh56ehQYBlXwqecmZ9P` above) for the next call. ## 4. Surface the highlight clips Episode highlights are AI-extracted clips — typically a handful per episode — scored for engagement and classified by type (`INSIGHTFUL`, `FUNNY`, `CONTROVERSIAL`, `AHA_MOMENT`, `NOTABLE_LINE`, and more). Pull the clips from the episode you just found. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/podcasts/episodes/2cgrh56ehQYBlXwqecmZ9P/clips?limit=3" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const episodeId = "2cgrh56ehQYBlXwqecmZ9P"; const res = await fetch( `https://api.particle.pro/v1/podcasts/episodes/${episodeId}/clips?limit=3`, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} episode_id = "2cgrh56ehQYBlXwqecmZ9P" res = httpx.get( f"https://api.particle.pro/v1/podcasts/episodes/{episode_id}/clips", params={"limit": 3}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "id": "53Fl0WgYU8H45pwOtkKHc2", "type": "INSIGHTFUL", "title": "AGI Sooner Than Expected", "description": "Sam Altman says AI progress is moving faster than even he predicted and hints that AGI could arrive sooner than people imagine.", "intro_statement": "Sam Altman evaluates how fast AGI approaches.", "engagement_score": 85, "duration_seconds": 37.62, "speaker": { "name": "Sam Altman", "role": "GUEST", "entity": { "slug": "sam-altman" } }, "audio_url": "https://cdn.particle.pro/podcast/episode/…/clip/audio/….mp3" } // … ], "has_more": true, "cursor": "…" } ``` `audio_url` is a direct MP3 of the clip — embed it, drop it into a player, or attach it to a social post. `intro_statement` is a ready-to-use lead-in for show notes or share copy. ## 5. Search dialogue by topic Mentions and episode filtering both pivot off *who* is being talked about. Search pivots off *what is being said*. Use `semantic_search` for paraphrase-tolerant retrieval — describe the topic in your own words and the engine finds segments that discuss the same idea, even when they use different vocabulary. ```bash curl theme={"dark"} curl --get "https://api.particle.pro/v1/podcasts/episodes/search" \ --data-urlencode "semantic_search=labor market disruption due to artificial intelligence" \ --data-urlencode "limit=2" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const url = new URL("https://api.particle.pro/v1/podcasts/episodes/search"); url.searchParams.set( "semantic_search", "labor market disruption due to artificial intelligence", ); url.searchParams.set("limit", "2"); const res = await fetch(url, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY }, }); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/podcasts/episodes/search", params={ "semantic_search": "labor market disruption due to artificial intelligence", "limit": 2, }, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```jsonc Response (truncated) theme={"dark"} { "data": [ { "episode": { "id": "371bZsGWsJYikBxoCej3GS", "title": "Where Investment Themes Intersect and Beat Markets", "podcast": { "title": "Thoughts on the Market", "slug": "thoughts-on-the-market" } }, "segment": { "type": "TOPIC_DISCUSSION", "title": "Labor Market Transformation from AI", "summary": "90% of occupations impacted\nSector analysis of employment effects\nNet 4% job loss with offsets from new hires\nShift toward role transformation over replacement", "start_seconds": 107.98, "end_seconds": 150.36 }, "windows": [ { "lines": [ { "speaker": "Stephen Byrd", "role": "HOST", "text": "Now, at the same time, AI is reshaping the labor market.", "is_match": true }, { "speaker": "Stephen Byrd", "role": "HOST", "text": "We estimate that automation or augmentation will impact ninety percent of occupations, so almost every job will be affected, but the effect is not binary.", "is_match": true } // …context lines around the matches ] } ], "match": { "source": "semantic", "relevance_score": 0.61 } } // … ], "has_more": true, "cursor": "…" } ``` Each result is one *segment* of one episode. The query never used the words "automation," "occupations," or "employment" — `semantic_search` matched on meaning. Lines whose text drove the match are flagged `is_match: true`. The response also returns any highlight clips that overlap the segment in the same payload. For exact-phrase matches (tickers, drug names, model numbers), pass `keyword_search` instead — or both, for hybrid retrieval. Add `entity_id` or `company_id` to scope a topical search to episodes featuring a specific person or company. The full surface is documented in [Search](/podcasts/search). ## 6. Pivot to companies Companies in Particle API are cross-referenced across every identifier system you might already have — SEC CIK, Wikidata QID, stock ticker, domain, and knowledge-graph slug. Look up Shopify by ticker: ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/companies?ticker=SHOP&limit=1" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/companies?ticker=SHOP&limit=1", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const { data } = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/companies", params={"ticker": "SHOP", "limit": 1}, headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) data = res.json()["data"] ``` ```json Response theme={"dark"} { "data": [ { "id": "21IMlzahxqyE3wPovDXXM9", "name": "Shopify", "description": "Shopify offers an e-commerce platform mainly to small and medium-sized businesses…", "identifiers": { "cik": "0001594805", "qid": "Q7501150", "entity": { "id": "17PzxG1tF2dy48", "slug": "shopify", "name": "Shopify" }, "ticker": "SHOP", "domain": "shopify.com" } } ], "has_more": false } ``` Path-style lookups accept the slug, the domain, or the canonical ID — these all return the same Shopify record (`id: 21IMlzahxqyE3wPovDXXM9`): ```bash theme={"dark"} curl "https://api.particle.pro/v1/companies/shopify" -H "X-API-Key: $PARTICLE_API_KEY" # by slug curl "https://api.particle.pro/v1/companies/shopify.com" -H "X-API-Key: $PARTICLE_API_KEY" # by domain ``` For ticker, CIK, or QID lookups use the list endpoint (`/v1/companies?ticker=SHOP`, `?cik=…`, `?qid=…`). The nested `entity.slug` (`shopify`) is the same kind of handle you used for Sam Altman in step 1 — feed it to any entity-aware endpoint to surface Shopify's mentions, episodes, and clips across the catalog. ## 7. Get sponsor analytics in one call Companies show up in podcasts in two ways: as topics of conversation *and* as advertisers. The per-company advertising endpoint gives you the sponsor view in a single request. ```bash curl theme={"dark"} curl "https://api.particle.pro/v1/companies/shopify/podcast/advertising" \ -H "X-API-Key: $PARTICLE_API_KEY" ``` ```js JavaScript theme={"dark"} const res = await fetch( "https://api.particle.pro/v1/companies/shopify/podcast/advertising", { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } }, ); const summary = await res.json(); ``` ```python Python theme={"dark"} res = httpx.get( "https://api.particle.pro/v1/companies/shopify/podcast/advertising", headers={"X-API-Key": os.environ["PARTICLE_API_KEY"]}, ) summary = res.json() ``` ```jsonc Response (truncated) theme={"dark"} { "company_id": "shopify.com", "total_ads": 9349, "podcast_reach": 578, "episode_reach": 8277, "read_type_breakdown": { "host_read": 7003, "pre_recorded": 2346 }, "recent_ads": [ { "sponsor_name": "Shopify", "product": "Shopify checkout", "offer_description": "Get a $1 trial when you switch to Shopify today", "sponsor_url": "shopify.com/setup", "read_type": "PRE_RECORDED", "placement_type": "PRE_ROLL", "start_seconds": 9.4, "end_seconds": 38.1, "duration_seconds": 28.7, "podcast": { "title": "The John Batchelor Show", "slug": "the-john-batchelor-show" }, "created_at": "2026-04-29T01:42:01.520819Z" } // … ] } ``` You get host-read vs pre-recorded breakdowns, recent placements with podcast attribution, and product-level tagging where available — plus the in-episode timing of each read. Enough to power competitive intelligence dashboards or category-spend analysis without scraping audio yourself. ## What's next IDs and slugs, pagination, errors, pricing weight, and choosing the right endpoint for the job. Episodes, transcripts, segments, clips, advertising, and bias. Every endpoint with request and response schemas.