Get Episode Transcripts via iTunes API
Find recent episodes with iTunes, match them to Pod Engine, and retrieve available transcript text.
Before you start
- API token
- An Apple Podcasts collection ID
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 Use Node.js 22 or newer. Save the complete script as recipe.mjs. No package installation is needed.
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
node recipe.mjs Use Python 3.10 or newer. Save the complete script as recipe.py.
python -m pip install podengine
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
python recipe.py Use Python 3.10 or newer. Save the complete script as recipe.py. No package installation is needed.
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
python recipe.py 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',
}); const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const baseUrl = process.env.PODENGINE_API_URL || 'https://api.podengine.ai';
async function request(path, body) {
const response = await fetch(`${baseUrl}${path}`, {
method: body === undefined ? 'GET' : 'POST',
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
if (!response.ok) throw new Error(`Pod Engine request failed: ${response.status} ${response.statusText}`);
return (await response.json()).data;
} import os
from podengine import PodEngine
pe = PodEngine(
api_key=os.environ["PODENGINE_API_KEY"],
base_url=os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai"),
) import json
import os
from urllib.parse import urlencode, quote
from urllib.request import Request, urlopen
api_key = os.environ["PODENGINE_API_KEY"]
base_url = os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai")
def request(path, body=None):
req = Request(
base_url + path,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": api_key, "Content-Type": "application/json"},
method="POST" if body is not None else "GET",
)
# urlopen raises HTTPError for non-success responses.
with urlopen(req, timeout=30) as response:
return json.load(response)["data"] Complete runnable 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',
});
const appleId = 1200361736;
const response = await fetch(
`https://itunes.apple.com/lookup?id=${appleId}&media=podcast&entity=podcastEpisode&limit=10`
);
if (!response.ok) throw new Error(`iTunes request failed: ${response.status}`);
const itunesData = await response.json();
const itunesEpisodes = itunesData.results.filter((item) => item.wrapperType === 'podcastEpisode');
console.log(`Found ${itunesEpisodes.length} iTunes episodes`);
const { podcast } = await pe.podcasts.getPodcastIdLookup({ appleId });
if (!podcast) throw new Error('This Apple podcast has not been matched to Pod Engine');
console.log(podcast.title, podcast.id);
const { podcastWithEpisodes } = await pe.podcasts.getPodcastEpisodes({ podcastIdOrSlug: podcast.id, limit: 100 });
// Only accept a unique exact audio URL or title match; skip ambiguous matches.
const matchedEpisodes = [];
for (const itunesEpisode of itunesEpisodes) {
let candidates = podcastWithEpisodes.episodes.filter((ep) => ep.enclosureUrl === itunesEpisode.episodeUrl);
if (!candidates.length) {
candidates = podcastWithEpisodes.episodes.filter(
(ep) => ep.title.trim().toLowerCase() === itunesEpisode.trackName.trim().toLowerCase()
);
}
if (candidates.length === 1) matchedEpisodes.push(candidates[0]);
else console.info(`No unique match: ${itunesEpisode.trackName}`);
}
// Set to true only when you want to request new transcriptions.
const requestMissingTranscripts = false;
const seen = new Set();
for (const episode of matchedEpisodes) {
if (seen.has(episode.episodeId)) continue;
seen.add(episode.episodeId);
if (!episode.hasTranscript) {
console.info(`No transcript available: ${episode.title}`);
if (requestMissingTranscripts) {
const result = await pe.transcriptions.requestEpisodeTranscription({ episodeId: episode.episodeId });
console.log('Transcription request:', result);
}
continue;
}
const { episodeTranscriptText } = await pe.episodes.getEpisodeTranscriptText({ episodeId: episode.episodeId });
console.log(episode.title, episodeTranscriptText.text?.slice(0, 500) ?? 'No transcript text available');
} const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const baseUrl = process.env.PODENGINE_API_URL || 'https://api.podengine.ai';
async function request(path, body) {
const response = await fetch(`${baseUrl}${path}`, {
method: body === undefined ? 'GET' : 'POST',
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
if (!response.ok) throw new Error(`Pod Engine request failed: ${response.status} ${response.statusText}`);
return (await response.json()).data;
}
const appleId = 1200361736;
const response = await fetch(
`https://itunes.apple.com/lookup?id=${appleId}&media=podcast&entity=podcastEpisode&limit=10`
);
if (!response.ok) throw new Error(`iTunes request failed: ${response.status}`);
const itunesData = await response.json();
const itunesEpisodes = itunesData.results.filter((item) => item.wrapperType === 'podcastEpisode');
console.log(`Found ${itunesEpisodes.length} iTunes episodes`);
const { podcast } = await request(`/api/v1/podcasts/id/lookup?appleId=${appleId}`);
if (!podcast) throw new Error('This Apple podcast has not been matched to Pod Engine');
console.log(podcast.title, podcast.id);
const { podcastWithEpisodes } = await request(`/api/v1/podcasts/${encodeURIComponent(podcast.id)}/episodes?limit=100`);
// Only accept a unique exact audio URL or title match; skip ambiguous matches.
const matchedEpisodes = [];
for (const itunesEpisode of itunesEpisodes) {
let candidates = podcastWithEpisodes.episodes.filter((ep) => ep.enclosureUrl === itunesEpisode.episodeUrl);
if (!candidates.length) {
candidates = podcastWithEpisodes.episodes.filter(
(ep) => ep.title.trim().toLowerCase() === itunesEpisode.trackName.trim().toLowerCase()
);
}
if (candidates.length === 1) matchedEpisodes.push(candidates[0]);
else console.info(`No unique match: ${itunesEpisode.trackName}`);
}
// Set to true only when you want to request new transcriptions.
const requestMissingTranscripts = false;
const seen = new Set();
for (const episode of matchedEpisodes) {
if (seen.has(episode.episodeId)) continue;
seen.add(episode.episodeId);
if (!episode.hasTranscript) {
console.info(`No transcript available: ${episode.title}`);
if (requestMissingTranscripts) {
const result = await request(`/api/v1/episodes/${encodeURIComponent(episode.episodeId)}/transcription-request`);
console.log('Transcription request:', result);
}
continue;
}
const { episodeTranscriptText } = await request(
`/api/v1/episodes/${encodeURIComponent(episode.episodeId)}/transcript-text`
);
console.log(episode.title, episodeTranscriptText.text?.slice(0, 500) ?? 'No transcript text available');
} import os
from podengine import PodEngine
pe = PodEngine(
api_key=os.environ["PODENGINE_API_KEY"],
base_url=os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai"),
)
import json
from urllib.request import urlopen
apple_id = 1200361736
with urlopen(
f"https://itunes.apple.com/lookup?id={apple_id}&media=podcast&entity=podcastEpisode&limit=10",
timeout=30,
) as response:
itunes_data = json.load(response)
itunes_episodes = [
item
for item in itunes_data["results"]
if item.get("wrapperType") == "podcastEpisode"
]
print("iTunes episodes:", len(itunes_episodes))
data = pe.podcasts.get_podcast_id_lookup(apple_id=apple_id).model_dump(
by_alias=True, mode="json"
)
podcast = data["podcast"]
if podcast is None:
raise RuntimeError("This Apple podcast has not been matched to Pod Engine")
print(podcast["title"], podcast["id"])
data = pe.podcasts.get_podcast_episodes(
podcast_id_or_slug=podcast["id"], limit=100
).model_dump(by_alias=True, mode="json")
episodes = data["podcastWithEpisodes"]["episodes"]
matched_episodes = []
for itunes_episode in itunes_episodes:
candidates = [
ep for ep in episodes if ep["enclosureUrl"] == itunes_episode.get("episodeUrl")
]
if not candidates:
candidates = [
ep
for ep in episodes
if ep["title"].strip().lower()
== itunes_episode["trackName"].strip().lower()
]
if len(candidates) == 1:
matched_episodes.append(candidates[0])
else:
print("No unique match:", itunes_episode["trackName"])
# Set to True only when you want to request new transcriptions.
request_missing_transcripts = False
seen = set()
for episode in matched_episodes:
if episode["episodeId"] in seen:
continue
seen.add(episode["episodeId"])
if not episode["hasTranscript"]:
print("No transcript available:", episode["title"])
if request_missing_transcripts:
result = pe.transcriptions.request_episode_transcription(
episode_id=episode["episodeId"]
)
print("Transcription request:", result)
continue
data = pe.episodes.get_episode_transcript_text(
episode_id=episode["episodeId"]
).model_dump(by_alias=True, mode="json")
text = data["episodeTranscriptText"]["text"]
print(episode["title"], text[:500] if text else "No transcript text available") import json
import os
from urllib.parse import urlencode, quote
from urllib.request import Request, urlopen
api_key = os.environ["PODENGINE_API_KEY"]
base_url = os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai")
def request(path, body=None):
req = Request(
base_url + path,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": api_key, "Content-Type": "application/json"},
method="POST" if body is not None else "GET",
)
# urlopen raises HTTPError for non-success responses.
with urlopen(req, timeout=30) as response:
return json.load(response)["data"]
import json
from urllib.request import urlopen
apple_id = 1200361736
with urlopen(
f"https://itunes.apple.com/lookup?id={apple_id}&media=podcast&entity=podcastEpisode&limit=10",
timeout=30,
) as response:
itunes_data = json.load(response)
itunes_episodes = [
item
for item in itunes_data["results"]
if item.get("wrapperType") == "podcastEpisode"
]
print("iTunes episodes:", len(itunes_episodes))
data = request(f"/api/v1/podcasts/id/lookup?appleId={apple_id}")
podcast = data["podcast"]
if podcast is None:
raise RuntimeError("This Apple podcast has not been matched to Pod Engine")
print(podcast["title"], podcast["id"])
data = request(f"/api/v1/podcasts/{quote(podcast['id'], safe='')}/episodes?limit=100")
episodes = data["podcastWithEpisodes"]["episodes"]
matched_episodes = []
for itunes_episode in itunes_episodes:
candidates = [
ep for ep in episodes if ep["enclosureUrl"] == itunes_episode.get("episodeUrl")
]
if not candidates:
candidates = [
ep
for ep in episodes
if ep["title"].strip().lower()
== itunes_episode["trackName"].strip().lower()
]
if len(candidates) == 1:
matched_episodes.append(candidates[0])
else:
print("No unique match:", itunes_episode["trackName"])
# Set to True only when you want to request new transcriptions.
request_missing_transcripts = False
seen = set()
for episode in matched_episodes:
if episode["episodeId"] in seen:
continue
seen.add(episode["episodeId"])
if not episode["hasTranscript"]:
print("No transcript available:", episode["title"])
if request_missing_transcripts:
result = request(
f"/api/v1/episodes/{quote(episode['episodeId'], safe='')}/transcription-request"
)
print("Transcription request:", result)
continue
data = request(
f"/api/v1/episodes/{quote(episode['episodeId'], safe='')}/transcript-text"
)
text = data["episodeTranscriptText"]["text"]
print(episode["title"], text[:500] if text else "No transcript text available") Use the Apple collection ID to request recent podcast episodes. Filter by wrapperType rather than assuming the first item is always a podcast. This external request uses a built-in HTTP client in both SDK and HTTP variants. See the Apple iTunes Search API documentation.
const appleId = 1200361736;
const response = await fetch(
`https://itunes.apple.com/lookup?id=${appleId}&media=podcast&entity=podcastEpisode&limit=10`
);
if (!response.ok) throw new Error(`iTunes request failed: ${response.status}`);
const itunesData = await response.json();
const itunesEpisodes = itunesData.results.filter((item) => item.wrapperType === 'podcastEpisode');
console.log(`Found ${itunesEpisodes.length} iTunes episodes`); const appleId = 1200361736;
const response = await fetch(
`https://itunes.apple.com/lookup?id=${appleId}&media=podcast&entity=podcastEpisode&limit=10`
);
if (!response.ok) throw new Error(`iTunes request failed: ${response.status}`);
const itunesData = await response.json();
const itunesEpisodes = itunesData.results.filter((item) => item.wrapperType === 'podcastEpisode');
console.log(`Found ${itunesEpisodes.length} iTunes episodes`); import json
from urllib.request import urlopen
apple_id = 1200361736
with urlopen(
f"https://itunes.apple.com/lookup?id={apple_id}&media=podcast&entity=podcastEpisode&limit=10",
timeout=30,
) as response:
itunes_data = json.load(response)
itunes_episodes = [
item
for item in itunes_data["results"]
if item.get("wrapperType") == "podcastEpisode"
]
print("iTunes episodes:", len(itunes_episodes)) import json
from urllib.request import urlopen
apple_id = 1200361736
with urlopen(
f"https://itunes.apple.com/lookup?id={apple_id}&media=podcast&entity=podcastEpisode&limit=10",
timeout=30,
) as response:
itunes_data = json.load(response)
itunes_episodes = [
item
for item in itunes_data["results"]
if item.get("wrapperType") == "podcastEpisode"
]
print("iTunes episodes:", len(itunes_episodes)) Resolve the Apple ID to a Pod Engine podcast. A lookup can return a null podcast, so stop with a clear message if the podcast has not been matched. Python SDK examples convert response models to dictionaries using API field names so the processing steps match the HTTP examples.
const { podcast } = await pe.podcasts.getPodcastIdLookup({ appleId });
if (!podcast) throw new Error('This Apple podcast has not been matched to Pod Engine');
console.log(podcast.title, podcast.id); const { podcast } = await request(`/api/v1/podcasts/id/lookup?appleId=${appleId}`);
if (!podcast) throw new Error('This Apple podcast has not been matched to Pod Engine');
console.log(podcast.title, podcast.id); data = pe.podcasts.get_podcast_id_lookup(apple_id=apple_id).model_dump(
by_alias=True, mode="json"
)
podcast = data["podcast"]
if podcast is None:
raise RuntimeError("This Apple podcast has not been matched to Pod Engine")
print(podcast["title"], podcast["id"]) data = request(f"/api/v1/podcasts/id/lookup?appleId={apple_id}")
podcast = data["podcast"]
if podcast is None:
raise RuntimeError("This Apple podcast has not been matched to Pod Engine")
print(podcast["title"], podcast["id"]) Compare the recent iTunes episodes with up to 100 Pod Engine episodes. Prefer an exact audio URL match, then fall back to an exact title match after trimming whitespace and ignoring case. Skip missing or ambiguous matches.
const { podcastWithEpisodes } = await pe.podcasts.getPodcastEpisodes({ podcastIdOrSlug: podcast.id, limit: 100 });
// Only accept a unique exact audio URL or title match; skip ambiguous matches.
const matchedEpisodes = [];
for (const itunesEpisode of itunesEpisodes) {
let candidates = podcastWithEpisodes.episodes.filter((ep) => ep.enclosureUrl === itunesEpisode.episodeUrl);
if (!candidates.length) {
candidates = podcastWithEpisodes.episodes.filter(
(ep) => ep.title.trim().toLowerCase() === itunesEpisode.trackName.trim().toLowerCase()
);
}
if (candidates.length === 1) matchedEpisodes.push(candidates[0]);
else console.info(`No unique match: ${itunesEpisode.trackName}`);
} const { podcastWithEpisodes } = await request(`/api/v1/podcasts/${encodeURIComponent(podcast.id)}/episodes?limit=100`);
// Only accept a unique exact audio URL or title match; skip ambiguous matches.
const matchedEpisodes = [];
for (const itunesEpisode of itunesEpisodes) {
let candidates = podcastWithEpisodes.episodes.filter((ep) => ep.enclosureUrl === itunesEpisode.episodeUrl);
if (!candidates.length) {
candidates = podcastWithEpisodes.episodes.filter(
(ep) => ep.title.trim().toLowerCase() === itunesEpisode.trackName.trim().toLowerCase()
);
}
if (candidates.length === 1) matchedEpisodes.push(candidates[0]);
else console.info(`No unique match: ${itunesEpisode.trackName}`);
} data = pe.podcasts.get_podcast_episodes(
podcast_id_or_slug=podcast["id"], limit=100
).model_dump(by_alias=True, mode="json")
episodes = data["podcastWithEpisodes"]["episodes"]
matched_episodes = []
for itunes_episode in itunes_episodes:
candidates = [
ep for ep in episodes if ep["enclosureUrl"] == itunes_episode.get("episodeUrl")
]
if not candidates:
candidates = [
ep
for ep in episodes
if ep["title"].strip().lower()
== itunes_episode["trackName"].strip().lower()
]
if len(candidates) == 1:
matched_episodes.append(candidates[0])
else:
print("No unique match:", itunes_episode["trackName"]) data = request(f"/api/v1/podcasts/{quote(podcast['id'], safe='')}/episodes?limit=100")
episodes = data["podcastWithEpisodes"]["episodes"]
matched_episodes = []
for itunes_episode in itunes_episodes:
candidates = [
ep for ep in episodes if ep["enclosureUrl"] == itunes_episode.get("episodeUrl")
]
if not candidates:
candidates = [
ep
for ep in episodes
if ep["title"].strip().lower()
== itunes_episode["trackName"].strip().lower()
]
if len(candidates) == 1:
matched_episodes.append(candidates[0])
else:
print("No unique match:", itunes_episode["trackName"]) - Title matches are candidates, not guaranteed identities. Verify episode dates and audio before using transcripts in an automated publication workflow.
- This example searches one page of episodes. For older episodes, paginate with skip and limit, or choose a smaller set of iTunes episodes.
Retrieve transcript text for uniquely matched episodes marked as having a transcript. Print the first 500 characters and skip duplicate episode IDs. The text field can be null. Missing transcripts are reported and skipped by default. To request them, enable the boolean switch at the start of this step. This sends a separate transcription request; it does not wait for the transcript to become available.
// Set to true only when you want to request new transcriptions.
const requestMissingTranscripts = false;
const seen = new Set();
for (const episode of matchedEpisodes) {
if (seen.has(episode.episodeId)) continue;
seen.add(episode.episodeId);
if (!episode.hasTranscript) {
console.info(`No transcript available: ${episode.title}`);
if (requestMissingTranscripts) {
const result = await pe.transcriptions.requestEpisodeTranscription({ episodeId: episode.episodeId });
console.log('Transcription request:', result);
}
continue;
}
const { episodeTranscriptText } = await pe.episodes.getEpisodeTranscriptText({ episodeId: episode.episodeId });
console.log(episode.title, episodeTranscriptText.text?.slice(0, 500) ?? 'No transcript text available');
} // Set to true only when you want to request new transcriptions.
const requestMissingTranscripts = false;
const seen = new Set();
for (const episode of matchedEpisodes) {
if (seen.has(episode.episodeId)) continue;
seen.add(episode.episodeId);
if (!episode.hasTranscript) {
console.info(`No transcript available: ${episode.title}`);
if (requestMissingTranscripts) {
const result = await request(`/api/v1/episodes/${encodeURIComponent(episode.episodeId)}/transcription-request`);
console.log('Transcription request:', result);
}
continue;
}
const { episodeTranscriptText } = await request(
`/api/v1/episodes/${encodeURIComponent(episode.episodeId)}/transcript-text`
);
console.log(episode.title, episodeTranscriptText.text?.slice(0, 500) ?? 'No transcript text available');
} # Set to True only when you want to request new transcriptions.
request_missing_transcripts = False
seen = set()
for episode in matched_episodes:
if episode["episodeId"] in seen:
continue
seen.add(episode["episodeId"])
if not episode["hasTranscript"]:
print("No transcript available:", episode["title"])
if request_missing_transcripts:
result = pe.transcriptions.request_episode_transcription(
episode_id=episode["episodeId"]
)
print("Transcription request:", result)
continue
data = pe.episodes.get_episode_transcript_text(
episode_id=episode["episodeId"]
).model_dump(by_alias=True, mode="json")
text = data["episodeTranscriptText"]["text"]
print(episode["title"], text[:500] if text else "No transcript text available") # Set to True only when you want to request new transcriptions.
request_missing_transcripts = False
seen = set()
for episode in matched_episodes:
if episode["episodeId"] in seen:
continue
seen.add(episode["episodeId"])
if not episode["hasTranscript"]:
print("No transcript available:", episode["title"])
if request_missing_transcripts:
result = request(
f"/api/v1/episodes/{quote(episode['episodeId'], safe='')}/transcription-request"
)
print("Transcription request:", result)
continue
data = request(
f"/api/v1/episodes/{quote(episode['episodeId'], safe='')}/transcript-text"
)
text = data["episodeTranscriptText"]["text"]
print(episode["title"], text[:500] if text else "No transcript text available")