ListenHubOpenAPI
API Reference

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_KEY

Create 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

FieldTypeRequiredDescription
querystringNoThe prompt or topic to generate from. May be empty when sources carries the material.
sourcesarrayNoReference material. Each item is { "type": "text" | "url", "content": "..." }. For url, content is the link; for text, content is the raw text.
speakersarrayYes1 to 2 speakers, each { "speakerId": "..." }. One speaker produces a monologue; two produce a conversation. debate mode requires exactly 2.
languagestringNoOutput language, e.g. en, zh, ja. When omitted, the language is inferred from the input.
modestringNoGeneration 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

ModeSpeakersBest for
quick1 or 2Fast turnaround on time-sensitive content. The default.
deep1 or 2In-depth, research-style episodes on professional topics.
debateExactly 2A 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

FieldTypeDescription
episodeIdstringThe episode identifier.
createdAtnumberCreation timestamp (epoch milliseconds).
processStatusstringOverall job status: pending, success, or fail.
contentStatusstringStage-level status, present in the two-stage workflow: text-success, text-fail, audio-success, audio-fail. Absent for one-shot generation.
failCodenumberFailure reason code; 0 when there is no failure.
messagestringHuman-readable status detail.
completedTimenumberCompletion timestamp (epoch milliseconds).
creditsnumberCredits consumed so far.
titlestringGenerated episode title.
outlinestringGenerated outline.
coverstringCover image URL.
audioUrlstringFinal audio file URL (MP3).
audioStreamUrlstringStreaming audio URL (HLS .m3u8).
subtitlesUrlstringSubtitle file URL (SRT).
sourceProcessResultobjectProcessed source material: content plus a references array of citations.
scriptsarrayPer-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:

  1. POST /v1/podcast/episodes/text-content — generate the script only.
  2. 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

ValueMeaningNext step
text-successScript generation completeRender audio
text-failScript generation failedRecreate the episode
audio-successAudio generation completeDone
audio-failAudio generation failedRetry 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.


On this page