API recipe

Track Keyword Mentions in Podcast Transcripts Every Day

Get every podcast episode that mentioned your topic yesterday, with the passages and links, as raw data you can store.

intermediate 20 minutes PodEngine Team
SearchEpisodesTranscriptsMonitoring

Before you start

  • API token
  • A topic and the words people use for it

Choose your example

Your selection applies to every step. HTTP uses built-in clients without the Pod Engine libraries.

Use Node.js 22 or newer. Save the complete script as recipe.mjs.

npm install @podengine/sdk
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
node recipe.mjs

Set up the client

Start your file with this setup, then append each step in order. The complete script below includes everything.

import { PodEngine } from '@podengine/sdk';

const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const pe = new PodEngine({
  apiKey,
  baseUrl: process.env.PODENGINE_API_URL || 'https://api.podengine.ai',
});
Complete runnable script
Download script
import { PodEngine } from '@podengine/sdk';

const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const pe = new PodEngine({
  apiKey,
  baseUrl: process.env.PODENGINE_API_URL || 'https://api.podengine.ai',
});

// Quotes make each term a phrase match.
const inTranscript = (terms) =>
  terms.map((term) => ({ searchTerm: `"${term}"`, searchType: 'text', searchTargets: ['transcript'] }));

// Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
const dayEnd = new Date();
dayEnd.setUTCHours(0, 0, 0, 0);
const dayStart = new Date(dayEnd.getTime() - 24 * 60 * 60 * 1000);

const phraseQuery = {
  searchTermGroups: [
    {
      searchTerms: inTranscript([
        'postpartum depression',
        'post-partum depression',
        'postnatal depression',
        'post-natal depression',
        'perinatal depression',
      ]),
    },
  ],
  publishedSince: dayStart.toISOString(),
  publishedBefore: dayEnd.toISOString(),
  transcriptHighlightLength: 300,
  pageSize: 100,
};
const phrasePage = await pe.search.searchEpisodes(phraseQuery);
console.log(`Episodes matching a phrase: ${phrasePage.result.total.value}`);
console.log(phrasePage.result.hits[0]?.transcriptHighlights);

const acronymQuery = {
  ...phraseQuery,
  searchTermGroups: [
    { searchTerms: inTranscript(['PPD', 'PND']) },
    {
      searchTerms: inTranscript([
        'postpartum',
        'postnatal',
        'perinatal',
        'pregnancy',
        'pregnant',
        'newborn',
        'giving birth',
        'new mom',
        'new mum',
      ]),
    },
  ],
};
const acronymPage = await pe.search.searchEpisodes(acronymQuery);
console.log(`Episodes matching an acronym in context: ${acronymPage.result.total.value}`);

const maxPerQuery = 5000;
const mentions = new Map();
for (const [label, query] of [
  ['phrase', phraseQuery],
  ['acronym', acronymQuery],
]) {
  let cursor = null;
  let fetched = 0;
  do {
    const page = await pe.search.searchEpisodes({ ...query, cursor });
    for (const hit of page.result.hits) {
      const mention = mentions.get(hit.episodeId) ?? { hit, matchedBy: [], passages: [] };
      if (!mention.matchedBy.includes(label)) mention.matchedBy.push(label);
      for (const passage of hit.transcriptHighlights ?? []) {
        if (!mention.passages.includes(passage)) mention.passages.push(passage);
      }
      mentions.set(hit.episodeId, mention);
    }
    fetched += page.result.hits.length;
    const nextCursor = page.cursor;
    if (!page.result.hits.length || !nextCursor || nextCursor === cursor) break;
    cursor = nextCursor;
  } while (fetched < maxPerQuery);
  if (fetched >= maxPerQuery) console.warn(`The ${label} search reached ${maxPerQuery} results; use a shorter window`);
}
console.log(`Episodes mentioning the topic on ${dayStart.toISOString().slice(0, 10)}: ${mentions.size}`);

const rows = [...mentions.values()].map(({ hit, matchedBy, passages }) => ({
  episodeId: hit.episodeId,
  published: hit.episodeRssPublishedAt,
  podcast: hit.podcastRssTitle,
  episode: hit.episodeRssTitle,
  matchedBy: matchedBy.join('+'),
  passages: passages.map((passage) => passage.replace(/<\/?em>/g, '')),
  estimatedMonthlyListeners: hit.podcastAudienceEstimatedMonthlyListeners ?? null,
  podEngineUrl: `https://www.podengine.ai/podcasts/${hit.podcastSlug}/${hit.episodeSlug}`,
  audioUrl: hit.episodeEnclosureUrl,
  applePodcastsUrl: hit.appleId ? `https://podcasts.apple.com/podcast/id${hit.appleId}` : null,
  spotifyUrl: hit.spotifyId ? `https://open.spotify.com/show/${hit.spotifyId}` : null,
}));
console.log(JSON.stringify(rows, null, 2));
1

Search every way people say the topic

This recipe is for research, social-listening and PR teams who need every podcast mention of a topic, every day, as raw data. The example tracks postpartum depression. Swap in your own topic and words.

Choose your language and SDK or HTTP above. Run the setup first, then append each step to the same file.

Send the Episode Search endpoint one searchTermGroups group that lists every spelled-out way of saying the topic. An episode matches when its transcript contains any one of the terms. Quotes around a term make it a phrase, so "postpartum depression" only matches those words together.

publishedSince and publishedBefore limit the search to yesterday in UTC. transcriptHighlightLength returns up to five passages around the matches in each hit's transcriptHighlights.

// Quotes make each term a phrase match.
const inTranscript = (terms) =>
  terms.map((term) => ({ searchTerm: `"${term}"`, searchType: 'text', searchTargets: ['transcript'] }));

// Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
const dayEnd = new Date();
dayEnd.setUTCHours(0, 0, 0, 0);
const dayStart = new Date(dayEnd.getTime() - 24 * 60 * 60 * 1000);

const phraseQuery = {
  searchTermGroups: [
    {
      searchTerms: inTranscript([
        'postpartum depression',
        'post-partum depression',
        'postnatal depression',
        'post-natal depression',
        'perinatal depression',
      ]),
    },
  ],
  publishedSince: dayStart.toISOString(),
  publishedBefore: dayEnd.toISOString(),
  transcriptHighlightLength: 300,
  pageSize: 100,
};
const phrasePage = await pe.search.searchEpisodes(phraseQuery);
console.log(`Episodes matching a phrase: ${phrasePage.result.total.value}`);
console.log(phrasePage.result.hits[0]?.transcriptHighlights);
2

Add acronyms, but only in context

Short forms are noisy on their own. When we tested "PPD" and "PND", they also matched police departments, a Chilean political party, a hair dye ingredient and slang.

Keep the acronyms, but require context. Put them in one group and context words in a second group. Groups combine with AND, so an episode has to say one acronym and at least one context word, such as "pregnant" or "newborn".

const acronymQuery = {
  ...phraseQuery,
  searchTermGroups: [
    { searchTerms: inTranscript(['PPD', 'PND']) },
    {
      searchTerms: inTranscript([
        'postpartum',
        'postnatal',
        'perinatal',
        'pregnancy',
        'pregnant',
        'newborn',
        'giving birth',
        'new mom',
        'new mum',
      ]),
    },
  ],
};
const acronymPage = await pe.search.searchEpisodes(acronymQuery);
console.log(`Episodes matching an acronym in context: ${acronymPage.result.total.value}`);
3

Pull the whole day's mentions

Run both searches for the day. Pass each response's cursor into the next request, and stop when a page has no hits or no new cursor.

A one-day window keeps each pull small, and you can rerun any day you missed. Schedule the program once a day, after midnight UTC.

const maxPerQuery = 5000;
const mentions = new Map();
for (const [label, query] of [
  ['phrase', phraseQuery],
  ['acronym', acronymQuery],
]) {
  let cursor = null;
  let fetched = 0;
  do {
    const page = await pe.search.searchEpisodes({ ...query, cursor });
    for (const hit of page.result.hits) {
      const mention = mentions.get(hit.episodeId) ?? { hit, matchedBy: [], passages: [] };
      if (!mention.matchedBy.includes(label)) mention.matchedBy.push(label);
      for (const passage of hit.transcriptHighlights ?? []) {
        if (!mention.passages.includes(passage)) mention.passages.push(passage);
      }
      mentions.set(hit.episodeId, mention);
    }
    fetched += page.result.hits.length;
    const nextCursor = page.cursor;
    if (!page.result.hits.length || !nextCursor || nextCursor === cursor) break;
    cursor = nextCursor;
  } while (fetched < maxPerQuery);
  if (fetched >= maxPerQuery) console.warn(`The ${label} search reached ${maxPerQuery} results; use a shorter window`);
}
console.log(`Episodes mentioning the topic on ${dayStart.toISOString().slice(0, 10)}: ${mentions.size}`);