Podcast
Generate single or dual-speaker podcast episodes — one-shot or split into a script-then-audio workflow — with quick, deep, and debate modes.
The Podcast API turns a prompt and optional reference sources into a fully produced episode: a written script with assigned voices, rendered audio, and subtitles. You can generate everything in one call, or split it into two stages — generate the script first, review or edit it, then render audio.
All requests use the base URL https://api.marswave.ai/openapi/v1 and require an API key:
Authorization: Bearer $LISTENHUB_API_KEYCreate keys at listenhub.ai/settings/api-keys. Every response is wrapped in { "code": 0, "message": "", "data": { ... } }; a non-zero code indicates an error.
Create a Podcast
POST /v1/podcast/episodes
Generate a complete episode (script + audio) in a single call. The request returns immediately with an episodeId; generation runs asynchronously, so poll the episode until it finishes.
Request parameters
| Field | Type | Required | Description |
|---|---|---|---|
query | string | No | The prompt or topic to generate from. May be empty when sources carries the material. |
sources | array | No | Reference material. Each item is { "type": "text" | "url", "content": "..." }. For url, content is the link; for text, content is the raw text. |
speakers | array | Yes | 1 to 2 speakers, each { "speakerId": "..." }. One speaker produces a monologue; two produce a conversation. debate mode requires exactly 2. |
language | string | No | Output language, e.g. en, zh, ja. When omitted, the language is inferred from the input. |
mode | string | No | Generation mode. One of quick, deep, debate. Defaults to quick. |
Provide at least one of query or sources. Look up speakerId values with the Speakers API.
Modes
| Mode | Speakers | Best for |
|---|---|---|
quick | 1 or 2 | Fast turnaround on time-sensitive content. The default. |
deep | 1 or 2 | In-depth, research-style episodes on professional topics. |
debate | Exactly 2 | A two-sided discussion where speakers argue distinct positions. |
Credit cost depends on the mode and length. Do not assume a fixed price — the credits field on the episode reflects the actual charge, and you can check your live balance with GET /v1/user/subscription. See Pricing for the credit-to-feature mapping.
Single-speaker example
A monologue in quick mode:
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Give a short technology news briefing for today.",
"speakers": [
{"speakerId": "<SPEAKER_ID_1>"}
],
"language": "en",
"mode": "quick"
}'const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'Give a short technology news briefing for today.',
speakers: [{ speakerId: '<SPEAKER_ID_1>' }],
language: 'en',
mode: 'quick',
}),
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/podcast/episodes',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'query': 'Give a short technology news briefing for today.',
'speakers': [{'speakerId': '<SPEAKER_ID_1>'}],
'language': 'en',
'mode': 'quick',
}
)
data = response.json()
print(data)Dual-speaker example
A two-host deep episode:
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Analyze the technical foundations and future outlook of large language models.",
"speakers": [
{"speakerId": "<SPEAKER_ID_1>"},
{"speakerId": "<SPEAKER_ID_2>"}
],
"language": "en",
"mode": "deep"
}'const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'Analyze the technical foundations and future outlook of large language models.',
speakers: [
{ speakerId: '<SPEAKER_ID_1>' },
{ speakerId: '<SPEAKER_ID_2>' },
],
language: 'en',
mode: 'deep',
}),
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/podcast/episodes',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'query': 'Analyze the technical foundations and future outlook of large language models.',
'speakers': [
{'speakerId': '<SPEAKER_ID_1>'},
{'speakerId': '<SPEAKER_ID_2>'},
],
'language': 'en',
'mode': 'deep',
}
)
data = response.json()
print(data)Debate mode
debate requires exactly 2 speakers:
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Should remote work become the default model?",
"speakers": [
{"speakerId": "<SPEAKER_ID_1>"},
{"speakerId": "<SPEAKER_ID_2>"}
],
"language": "en",
"mode": "debate"
}'const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'Should remote work become the default model?',
speakers: [
{ speakerId: '<SPEAKER_ID_1>' },
{ speakerId: '<SPEAKER_ID_2>' },
],
language: 'en',
mode: 'debate',
}),
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/podcast/episodes',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'query': 'Should remote work become the default model?',
'speakers': [
{'speakerId': '<SPEAKER_ID_1>'},
{'speakerId': '<SPEAKER_ID_2>'},
],
'language': 'en',
'mode': 'debate',
}
)
data = response.json()
print(data)With reference sources
Pass sources to ground the episode in specific material. Each entry is either a URL to fetch or raw text:
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Summarize and discuss the core ideas in this article.",
"sources": [
{
"type": "url",
"content": "https://blog.samaltman.com/reflections"
}
],
"speakers": [
{"speakerId": "<SPEAKER_ID_1>"},
{"speakerId": "<SPEAKER_ID_2>"}
],
"language": "en",
"mode": "deep"
}'const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'Summarize and discuss the core ideas in this article.',
sources: [
{ type: 'url', content: 'https://blog.samaltman.com/reflections' },
],
speakers: [
{ speakerId: '<SPEAKER_ID_1>' },
{ speakerId: '<SPEAKER_ID_2>' },
],
language: 'en',
mode: 'deep',
}),
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/podcast/episodes',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'query': 'Summarize and discuss the core ideas in this article.',
'sources': [
{'type': 'url', 'content': 'https://blog.samaltman.com/reflections'}
],
'speakers': [
{'speakerId': '<SPEAKER_ID_1>'},
{'speakerId': '<SPEAKER_ID_2>'},
],
'language': 'en',
'mode': 'deep',
}
)
data = response.json()
print(data)Response
{
"code": 0,
"message": "",
"data": {
"episodeId": "665f1c2a9b3e4d0012a8c7e1"
}
}episodeId is the handle for every follow-up call. Save it and poll for status.
Query Episode Status
GET /v1/podcast/episodes/{episodeId}
Fetch the current state and, once finished, the produced assets. Poll this endpoint after creating an episode until generation completes.
curl -X GET "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"const response = await fetch(
`https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}`,
{ headers: { 'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}` } }
);
const data = await response.json();
console.log('Status:', data.data.processStatus);
console.log('Audio URL:', data.data.audioUrl);import os
import requests
response = requests.get(
f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'}
)
data = response.json()
print('Status:', data['data']['processStatus'])
print('Audio URL:', data['data']['audioUrl'])Response fields
| Field | Type | Description |
|---|---|---|
episodeId | string | The episode identifier. |
createdAt | number | Creation timestamp (epoch milliseconds). |
processStatus | string | Overall job status: pending, success, or fail. |
contentStatus | string | Stage-level status, present in the two-stage workflow: text-success, text-fail, audio-success, audio-fail. Absent for one-shot generation. |
failCode | number | Failure reason code; 0 when there is no failure. |
message | string | Human-readable status detail. |
completedTime | number | Completion timestamp (epoch milliseconds). |
credits | number | Credits consumed so far. |
title | string | Generated episode title. |
outline | string | Generated outline. |
cover | string | Cover image URL. |
audioUrl | string | Final audio file URL (MP3). |
audioStreamUrl | string | Streaming audio URL (HLS .m3u8). |
subtitlesUrl | string | Subtitle file URL (SRT). |
sourceProcessResult | object | Processed source material: content plus a references array of citations. |
scripts | array | Per-line script: each item is { "speakerId", "speakerName", "content" }. |
Response when generation is complete (processStatus: "success"):
{
"code": 0,
"message": "",
"data": {
"episodeId": "665f1c2a9b3e4d0012a8c7e1",
"processStatus": "success",
"failCode": 0,
"credits": 27,
"title": "The Story of AI: How the Intelligent Age Was Born",
"audioUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a8c7e1.mp3",
"audioStreamUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a8c7e1.m3u8",
"scripts": [
{
"speakerId": "<SPEAKER_ID_1>",
"speakerName": "Ethan",
"content": "These days it feels like AI news surrounds us everywhere."
}
]
}
}Podcast generation typically takes 1 to 4 minutes. Recommended polling: wait 60 seconds before the first request, then poll every 10 seconds. On failure, processStatus is fail and failCode indicates the reason.
Stream script and outline (SSE)
GET /v1/podcast/episodes/{episodeId}/text-stream?event={script|outline}
While the script or outline is being generated, subscribe to a Server-Sent Events stream to receive text incrementally instead of polling. The event query parameter selects which stream:
outline— the outline as it is written.script— the line-by-line script as it is written.
curl -N "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}/text-stream?event=script" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"The response is an text/event-stream; read it as a stream rather than parsing it as a single JSON body. Use it for live progress UIs. To read the final assets (audio, subtitles), use the status endpoint once generation completes.
Two-Stage Generation
Split generation into two independent calls: produce the script first, inspect or edit it, then render audio. This is the right pattern when you need a human or automated review step before committing to audio, or when you want to account for text-generation and audio-generation credits separately.
The flow has two endpoints:
POST /v1/podcast/episodes/text-content— generate the script only.POST /v1/podcast/episodes/{episodeId}/audio— render audio from the (optionally edited) script.
language is required in text-content, and each speaker's language must match the language value. A mismatch returns a Speaker language mismatch error.
Generate the script
POST /v1/podcast/episodes/text-content accepts the same query, sources, speakers, and mode fields as one-shot creation, but language is required here. It produces the script with no audio:
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes/text-content" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "Discuss the current state of quantum computing.",
"speakers": [
{"speakerId": "<SPEAKER_ID_1>"},
{"speakerId": "<SPEAKER_ID_2>"}
],
"language": "en",
"mode": "deep"
}'const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes/text-content', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'Discuss the current state of quantum computing.',
speakers: [
{ speakerId: '<SPEAKER_ID_1>' },
{ speakerId: '<SPEAKER_ID_2>' },
],
language: 'en',
mode: 'deep',
}),
});
const data = await response.json();
const episodeId = data.data.episodeId;
console.log('Episode ID:', episodeId);import os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/podcast/episodes/text-content',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'query': 'Discuss the current state of quantum computing.',
'speakers': [
{'speakerId': '<SPEAKER_ID_1>'},
{'speakerId': '<SPEAKER_ID_2>'},
],
'language': 'en',
'mode': 'deep',
}
)
data = response.json()
episode_id = data['data']['episodeId']
print('Episode ID:', episode_id)Response:
{
"code": 0,
"message": "",
"data": {
"episodeId": "665f1c2a9b3e4d0012a8c7e1",
"message": "Text content generation started. Audio generation can be triggered later."
}
}Wait for the script
Poll GET /v1/podcast/episodes/{episodeId} until contentStatus is text-success. In the two-stage flow, contentStatus is the field that tells you which stage is done — check it rather than processStatus.
curl -X GET "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"const result = await fetch(`https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}`, {
headers: { 'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}` },
});
const status = await result.json();
console.log('Content status:', status.data.contentStatus);import os
import requests
result = requests.get(
f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'}
)
status = result.json()
print('Content status:', status['data']['contentStatus'])Script-ready response (contentStatus: "text-success"):
{
"code": 0,
"message": "",
"data": {
"episodeId": "665f1c2a9b3e4d0012a8c7e1",
"processStatus": "success",
"contentStatus": "text-success",
"credits": 15,
"title": "Quantum Computing: Present and Future",
"outline": "...",
"scripts": [
{
"speakerId": "<SPEAKER_ID_1>",
"speakerName": "Ethan",
"content": "Welcome to this discussion on quantum computing..."
},
{
"speakerId": "<SPEAKER_ID_2>",
"speakerName": "Sophia",
"content": "Quantum computing is exciting and rapidly evolving..."
}
]
}
}For a live progress UI, subscribe to the SSE stream at /v1/podcast/episodes/{episodeId}/text-stream?event=script instead of polling.
(Optional) Edit the script
Take the scripts array from the previous response and rewrite line content as needed.
Only content may change. Keep each line's speakerId exactly as returned, and keep the script to the same 1 to 2 distinct speakers — this is a hard API constraint.
Pass the edited array as the request body in the next step.
Render audio
POST /v1/podcast/episodes/{episodeId}/audio. Send an empty body to render the original script, or pass a scripts array (each item { "content", "speakerId" }) to render your edited version:
# Render the original script
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}/audio" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json"
# Render an edited script
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}/audio" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scripts": [
{
"content": "Welcome to this episode. Today we go deeper into quantum computing...",
"speakerId": "<SPEAKER_ID_1>"
},
{
"content": "This field has moved quickly from theory to practical experiments...",
"speakerId": "<SPEAKER_ID_2>"
}
]
}'// Render the original script
await fetch(`https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}/audio`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
});
// Render an edited script
await fetch(`https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}/audio`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
scripts: [
{ content: 'Welcome to this episode. Today we go deeper into quantum computing...', speakerId: '<SPEAKER_ID_1>' },
{ content: 'This field has moved quickly from theory to practical experiments...', speakerId: '<SPEAKER_ID_2>' },
],
}),
});import os
import requests
# Render the original script
requests.post(
f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}/audio',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'}
)
# Render an edited script
requests.post(
f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}/audio',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'scripts': [
{'content': 'Welcome to this episode. Today we go deeper into quantum computing...', 'speakerId': '<SPEAKER_ID_1>'},
{'content': 'This field has moved quickly from theory to practical experiments...', 'speakerId': '<SPEAKER_ID_2>'},
]
}
)Response:
{
"code": 0,
"message": "",
"data": {
"success": true,
"message": "Audio generation started",
"episodeId": "665f1c2a9b3e4d0012a8c7e1",
"status": "submit"
}
}Wait for audio
Continue polling GET /v1/podcast/episodes/{episodeId} until contentStatus is audio-success. The audio, streaming, and subtitle URLs are then populated:
{
"code": 0,
"message": "",
"data": {
"episodeId": "665f1c2a9b3e4d0012a8c7e1",
"processStatus": "success",
"contentStatus": "audio-success",
"credits": 42,
"title": "Quantum Computing: Present and Future",
"audioUrl": "https://assets.listenhub.ai/podcast/665f1c2a9b3e4d0012a8c7e1.mp3",
"audioStreamUrl": "https://assets.listenhub.ai/podcast/665f1c2a9b3e4d0012a8c7e1.m3u8",
"subtitlesUrl": "https://assets.listenhub.ai/podcast/665f1c2a9b3e4d0012a8c7e1.srt",
"scripts": [ ]
}
}contentStatus reference
| Value | Meaning | Next step |
|---|---|---|
text-success | Script generation complete | Render audio |
text-fail | Script generation failed | Recreate the episode |
audio-success | Audio generation complete | Done |
audio-fail | Audio generation failed | Retry audio rendering |
Credits: Stage 1 consumes text-generation credits; Stage 2 consumes audio-generation credits. The credits field accumulates across both stages and reflects the actual charge. Check your live balance with GET /v1/user/subscription, and see Pricing for the credit-to-feature mapping.