ListenHubOpenAPI
API Reference

Text to Speech

Convert text to natural-sounding speech across five endpoints, from low-latency single-voice streaming to long-form async narration.

ListenHub exposes several text-to-speech endpoints, each tuned for a different shape of work. They share the same base URL, authentication, and speaker IDs, but differ in latency, response type, and how many voices they support.

All requests go to https://api.marswave.ai/openapi/v1 and authenticate with an API key:

Authorization: Bearer $LISTENHUB_API_KEY

Create keys at listenhub.ai/settings/api-keys. Every JSON response is wrapped as { "code": 0, "message": "", "data": { ... } }; a non-zero code signals an error. The streaming endpoints (/v1/tts, /v1/audio/speech) return raw binary audio instead of this envelope.

Choosing an endpoint

EndpointVoicesSync / AsyncResponseBest for
POST /v1/ttsSingleSyncBinary audio streamReal-time playback, in-app voice, low latency
POST /v1/audio/speechSingleSyncBinary audio streamDrop-in replacement for the OpenAI TTS endpoint
POST /v1/speechMultipleSyncJSON with audioUrlDialogue, audiobooks, prepared multi-voice scripts
POST /v1/flow-speech/episodesSingleAsyncPoll by episodeIdArticle and newsletter narration, URL-to-audio
POST /v1/flow-speech/episodes/ttsMultipleAsyncPoll by episodeIdLong multi-voice scripts converted verbatim

Rule of thumb: reach for /v1/tts when you need audio bytes back immediately for one voice, /v1/speech when you have a short multi-voice script and want a hosted URL in one call, and the /v1/flow-speech/episodes endpoints when the job is long enough to run in the background.

Credit cost scales with audio length and is returned on the relevant responses (credits) once a job completes. To estimate cost before generating, see the credits estimation endpoints in the API reference.


Streaming TTS

POST /v1/tts

Low-latency single-voice synthesis. The response body is raw binary audio streamed as it is generated, so the first bytes arrive before the full clip is ready. Use this for real-time playback and interactive voice features.

This endpoint accepts the OpenAI text-to-speech request shape (input / voice / response_format), which makes it easy to migrate existing clients.

curl -X POST "https://api.marswave.ai/openapi/v1/tts" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Hello, welcome to ListenHub text-to-speech.",
    "voice": "EN-Man-General-01",
    "response_format": "mp3"
  }' \
  --output output.mp3
const response = await fetch('https://api.marswave.ai/openapi/v1/tts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    input: 'Hello, welcome to ListenHub text-to-speech.',
    voice: 'EN-Man-General-01',
    response_format: 'mp3',
  }),
});
const buffer = Buffer.from(await response.arrayBuffer());
// Write `buffer` to a file, or pipe `response.body` to a player
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/tts',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'input': 'Hello, welcome to ListenHub text-to-speech.',
        'voice': 'EN-Man-General-01',
        'response_format': 'mp3',
    },
    stream=True,
)

with open('output.mp3', 'wb') as f:
    for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)

Request parameters

FieldTypeRequiredDescription
inputstringYesText to synthesize. Max 20,000 characters.
voicestringYesSpeaker ID (the speakerId value from Speakers).
response_formatstringNoRequested audio format. One of mp3, opus, aac, flac, wav, pcm. Defaults to mp3.

The response body is binary audio, not the JSON envelope — read it as a stream or blob. The delivered container is MP3 (Content-Type: audio/mpeg) for every format except opus, which is delivered as OGG/Opus (Content-Type: audio/ogg). If the request fails before audio starts, the response falls back to a JSON error object, so check the Content-Type before treating the body as audio.


OpenAI-compatible TTS

POST /v1/audio/speech

An exact alias of /v1/tts at the path the OpenAI SDK and OpenAI-compatible integrations call by default. The request body, response_format options, and streaming binary response are identical. Point an existing OpenAI TTS client at this URL with your ListenHub API key and a ListenHub voice ID to switch over without code changes.

curl -X POST "https://api.marswave.ai/openapi/v1/audio/speech" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "This endpoint mirrors the OpenAI speech API.",
    "voice": "EN-Woman-General-01",
    "response_format": "mp3"
  }' \
  --output output.mp3
const response = await fetch('https://api.marswave.ai/openapi/v1/audio/speech', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    input: 'This endpoint mirrors the OpenAI speech API.',
    voice: 'EN-Woman-General-01',
    response_format: 'mp3',
  }),
});
const buffer = Buffer.from(await response.arrayBuffer());
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/audio/speech',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'input': 'This endpoint mirrors the OpenAI speech API.',
        'voice': 'EN-Woman-General-01',
        'response_format': 'mp3',
    },
    stream=True,
)

with open('output.mp3', 'wb') as f:
    for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)

The request parameters and response behavior are the same as Streaming TTS.


Multi-Speaker Script to Audio

POST /v1/speech

Generate one audio file from a prepared multi-voice script. Each line carries its own speakerId, so you can alternate voices for dialogue. The call is synchronous and returns a hosted audio URL plus subtitles in the response — no polling.

curl -X POST "https://api.marswave.ai/openapi/v1/speech" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scripts": [
      { "content": "Welcome everyone to this episode.", "speakerId": "EN-Man-General-01" },
      { "content": "Today we are discussing an interesting topic.", "speakerId": "EN-Woman-General-01" },
      { "content": "Great, let us begin.", "speakerId": "EN-Man-General-01" }
    ]
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/speech', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    scripts: [
      { content: 'Welcome everyone to this episode.', speakerId: 'EN-Man-General-01' },
      { content: 'Today we are discussing an interesting topic.', speakerId: 'EN-Woman-General-01' },
      { content: 'Great, let us begin.', speakerId: 'EN-Man-General-01' },
    ],
  }),
});
const { data } = await response.json();
console.log(data.audioUrl);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/speech',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'scripts': [
            {'content': 'Welcome everyone to this episode.', 'speakerId': 'EN-Man-General-01'},
            {'content': 'Today we are discussing an interesting topic.', 'speakerId': 'EN-Woman-General-01'},
            {'content': 'Great, let us begin.', 'speakerId': 'EN-Man-General-01'},
        ]
    },
)
data = response.json()['data']
print(data['audioUrl'])

Request parameters

FieldTypeRequiredDescription
scriptsarrayYesOne or more script lines, synthesized in order.
scripts[].contentstringYesLine text. Must be non-empty; combined length across all lines is capped at 20,000 characters.
scripts[].speakerIdstringYesSpeaker ID for this line. Different lines may use different speakers.

Response

{
  "code": 0,
  "message": "",
  "data": {
    "audioUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/example.mp3",
    "audioDuration": 12500,
    "subtitlesUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/example.srt",
    "taskId": "1eed39d387a046c0a1213e6b8f139d77",
    "credits": 12
  }
}
FieldTypeDescription
audioUrlstringURL of the generated MP3 file.
audioDurationintegerAudio duration in milliseconds.
subtitlesUrlstringSRT subtitle file URL. Valid for 24 hours.
taskIdstringTask ID. Quote it when reporting an issue so support can locate the request.
creditsintegerCredits consumed by this request.

Long-Form Text to Speech

POST /v1/flow-speech/episodes

Convert a block of text or the contents of a URL into a single-voice narration. This endpoint runs asynchronously: the request returns an episodeId right away, and you poll for the audio once it finishes. It is built for longer inputs where waiting on a synchronous call would be impractical.

Two modes control how the source text is handled:

  • smart (default) — cleans up the text first: fixes punctuation, grammar, and formatting so rough or pasted input still reads naturally.
  • direct — synthesizes the text verbatim, with no rewriting. Use this when the script is already final.

Constraints: exactly one sources item, exactly one speaker, and a text source of at least 10 characters (up to 20,000).

Smart mode (AI polish)

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      {
        "type": "text",
        "content": "welcome to listenhub this text is intentionally rough and punctuation will be improved automatically"
      }
    ],
    "speakers": [
      { "speakerId": "EN-Woman-General-01" }
    ],
    "language": "en",
    "mode": "smart"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sources: [
      {
        type: 'text',
        content: 'welcome to listenhub this text is intentionally rough and punctuation will be improved automatically',
      },
    ],
    speakers: [{ speakerId: 'EN-Woman-General-01' }],
    language: 'en',
    mode: 'smart',
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'sources': [
            {
                'type': 'text',
                'content': 'welcome to listenhub this text is intentionally rough and punctuation will be improved automatically',
            }
        ],
        'speakers': [{'speakerId': 'EN-Woman-General-01'}],
        'language': 'en',
        'mode': 'smart',
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

Direct mode

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      {
        "type": "text",
        "content": "Welcome to ListenHub. This script is already finalized and should be converted as-is."
      }
    ],
    "speakers": [
      { "speakerId": "EN-Man-General-01" }
    ],
    "language": "en",
    "mode": "direct"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sources: [
      {
        type: 'text',
        content: 'Welcome to ListenHub. This script is already finalized and should be converted as-is.',
      },
    ],
    speakers: [{ speakerId: 'EN-Man-General-01' }],
    language: 'en',
    mode: 'direct',
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'sources': [
            {
                'type': 'text',
                'content': 'Welcome to ListenHub. This script is already finalized and should be converted as-is.',
            }
        ],
        'speakers': [{'speakerId': 'EN-Man-General-01'}],
        'language': 'en',
        'mode': 'direct',
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

Read content from a URL

Set type to url and pass the page address in uri. ListenHub fetches and extracts the readable content before synthesis. (content is still accepted in place of uri for backward compatibility.)

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      {
        "type": "url",
        "uri": "https://example.com/article.html"
      }
    ],
    "speakers": [
      { "speakerId": "EN-Woman-General-01" }
    ],
    "language": "en",
    "mode": "smart"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sources: [{ type: 'url', uri: 'https://example.com/article.html' }],
    speakers: [{ speakerId: 'EN-Woman-General-01' }],
    language: 'en',
    mode: 'smart',
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'sources': [{'type': 'url', 'uri': 'https://example.com/article.html'}],
        'speakers': [{'speakerId': 'EN-Woman-General-01'}],
        'language': 'en',
        'mode': 'smart',
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

Request parameters

FieldTypeRequiredDescription
sourcesarrayYesContent source. Exactly one item.
sources[].typestringYestext or url.
sources[].contentstringFor textText to narrate. Minimum 10 characters, maximum 20,000. For very short clips, use /v1/speech instead.
sources[].uristringFor url (recommended)Page URL to read from. Either uri or content must be present for a url source.
speakersarrayYesSpeaker list. Exactly one item.
speakers[].speakerIdstringYesSpeaker ID.
languagestringNoSource language: en, zh, or ja. Inferred from the content when omitted.
modestringNosmart (AI polish) or direct (verbatim). Defaults to smart.

The response contains only the task ID:

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a4c8e1"
  }
}

Poll for results

GET /v1/flow-speech/episodes/{episodeId}

Poll with the returned episodeId until processStatus is success.

curl -X GET "https://api.marswave.ai/openapi/v1/flow-speech/episodes/{episodeId}" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"
const response = await fetch(
  `https://api.marswave.ai/openapi/v1/flow-speech/episodes/${episodeId}`,
  { headers: { Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}` } },
);
const { data } = await response.json();
console.log('Status:', data.processStatus);
console.log('Audio URL:', data.audioUrl);
import os
import requests

response = requests.get(
    f'https://api.marswave.ai/openapi/v1/flow-speech/episodes/{episode_id}',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
)
data = response.json()['data']
print('Status:', data['processStatus'])
print('Audio URL:', data.get('audioUrl'))

When the job finishes (processStatus is success):

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a4c8e1",
    "createdAt": 1717430000000,
    "processStatus": "success",
    "completedTime": 1717430090000,
    "title": "Article Title",
    "outline": "...",
    "cover": "https://assets.listenhub.ai/.../cover.png",
    "audioUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a4c8e1.mp3",
    "audioStreamUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a4c8e1.m3u8",
    "subtitlesUrl": "https://assets.listenhub.ai/.../665f1c2a9b3e4d0012a4c8e1.srt",
    "scripts": "Full narration script text..."
  }
}
FieldTypeDescription
episodeIdstringThe episode identifier.
createdAtintegerCreation timestamp in milliseconds.
processStatusstringCurrent state: pending, success, or fail. Poll until success; fail indicates the job did not complete.
failCodeintegerPresent on failure; identifies the reason.
completedTimeintegerCompletion timestamp in milliseconds.
titlestringGenerated episode title.
outlinestringGenerated outline of the narration.
coverstringCover image URL.
audioUrlstringMP3 audio file URL.
audioStreamUrlstringHLS streaming URL (.m3u8).
subtitlesUrlstringSRT subtitle file URL.
scriptsstringFull narration script text.

Long-form jobs typically finish in one to two minutes. A practical polling strategy: wait 30 seconds after creation, then poll every 10 seconds. On failure, processStatus is fail and failCode carries the reason.


Multi-Speaker Direct (async)

POST /v1/flow-speech/episodes/tts

Convert a long, prepared multi-voice script into an episode. This is the asynchronous, multi-speaker counterpart to /v1/speech: every line keeps its own speakerId, the text is synthesized verbatim (direct mode), and the call returns an episodeId to poll. Reach for it when a multi-voice script is too long to handle in a single synchronous /v1/speech request.

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes/tts" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Roundtable Discussion",
    "scripts": [
      { "content": "Thanks for joining the roundtable today.", "speakerId": "EN-Man-General-01" },
      { "content": "Happy to be here. Let us dig into the agenda.", "speakerId": "EN-Woman-General-01" }
    ]
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes/tts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'Roundtable Discussion',
    scripts: [
      { content: 'Thanks for joining the roundtable today.', speakerId: 'EN-Man-General-01' },
      { content: 'Happy to be here. Let us dig into the agenda.', speakerId: 'EN-Woman-General-01' },
    ],
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes/tts',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'title': 'Roundtable Discussion',
        'scripts': [
            {'content': 'Thanks for joining the roundtable today.', 'speakerId': 'EN-Man-General-01'},
            {'content': 'Happy to be here. Let us dig into the agenda.', 'speakerId': 'EN-Woman-General-01'},
        ],
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

Request parameters

FieldTypeRequiredDescription
scriptsarrayYesOne or more script lines, synthesized in order.
scripts[].contentstringYesLine text. Must be non-empty; combined length across all lines is capped at 20,000 characters.
scripts[].speakerIdstringYesSpeaker ID for this line. Different lines may use different speakers.
titlestringNoCustom episode title. Auto-generated when omitted.

The response returns an episodeId. Poll for results with the same status endpoint as long-form jobs, GET /v1/flow-speech/episodes/{episodeId}.

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a4c8e1"
  }
}

On this page