Examples
Eight runnable TypeScript recipes for the OpenAPIClient — podcast, flow speech, TTS, image, video, music, content extract, and error handling.
A cookbook of complete, copy-paste recipes built on the OpenAPIClient (API key, server-side). Each one is a self-contained TypeScript file you can run with tsx. They share two conventions worth stating once:
- Construct the client from an environment variable.
new OpenAPIClient()readsLISTENHUB_API_KEY. Create a key at listenhub.ai/settings/api-keys and keep it server-side. See Authentication. - Generation is asynchronous. A create call returns an
episodeIdortaskIdimmediately; you poll aget*method until the status leavespending/generating. Every recipe below uses the samesleephelper for its poll loop.
Run any recipe with LISTENHUB_API_KEY=lh_sk_... npx tsx recipe.ts. The SDK is ESM-only and requires Node.js >= 20. For user-facing apps that act on behalf of a logged-in user, use ListenHubClient with OAuth instead — see Authentication.
Create a podcast and poll until done
A two-host podcast grounded in a URL. List speakers for the language, start the episode, then poll getPodcast until processStatus leaves pending.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient(); // reads LISTENHUB_API_KEY
// Pick two voices for the language.
const { items: speakers } = await client.listSpeakers({ language: 'en' });
const [host, guest] = speakers;
const { episodeId } = await client.createPodcast({
query: 'Explain how transformers work in large language models',
sources: [
{
type: 'url',
content: 'https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)',
},
],
speakers: [{ speakerId: host.speakerId }, { speakerId: guest.speakerId }],
language: 'en',
});
console.log(`Created podcast: ${episodeId}`);
let detail = await client.getPodcast(episodeId);
while (detail.processStatus === 'pending') {
await sleep(5000);
detail = await client.getPodcast(episodeId);
console.log(`Status: ${detail.processStatus}`);
}
if (detail.audioUrl) {
console.log(`Title: ${detail.title}`);
console.log(`Audio: ${detail.audioUrl}`);
} else {
console.error(`Generation failed (failCode ${detail.failCode}): ${detail.message}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}Multi-speaker flow speech
Flow speech narrates text or a web page with one or more voices. Pass several entries in speakers to alternate them across the script. The create-and-poll shape matches the podcast recipe — only the method names change.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const { items: speakers } = await client.listSpeakers({ language: 'en' });
const [voiceA, voiceB] = speakers;
const { episodeId } = await client.createFlowSpeech({
sources: [{ type: 'url', uri: 'https://en.wikipedia.org/wiki/Mars' }],
speakers: [{ speakerId: voiceA.speakerId }, { speakerId: voiceB.speakerId }],
language: 'en',
mode: 'smart', // 'smart' rewrites the source into a script; 'direct' reads it as-is
});
console.log(`Created flow speech: ${episodeId}`);
let detail = await client.getFlowSpeech(episodeId);
while (detail.processStatus === 'pending') {
await sleep(3000);
detail = await client.getFlowSpeech(episodeId);
console.log(`Status: ${detail.processStatus}`);
}
if (detail.audioUrl) {
console.log(`Title: ${detail.title}`);
console.log(`Audio: ${detail.audioUrl}`);
} else {
console.error(`Generation failed (failCode ${detail.failCode}): ${detail.message}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}Text-to-speech, saved to a file
tts is synchronous and returns the raw audio Response (no envelope, no polling). Stream the body straight to disk. Pass a voice (a speakerId from listSpeakers) and an optional response_format.
import { writeFile } from 'node:fs/promises';
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const { items: speakers } = await client.listSpeakers({ language: 'en' });
const voice = speakers[0];
// `tts` returns the raw Response — read its bytes directly.
const response = await client.tts({
input: 'Hello world, this is ListenHub speaking.',
voice: voice.speakerId,
response_format: 'mp3', // 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'
});
const audio = Buffer.from(await response.arrayBuffer());
await writeFile('speech.mp3', audio);
console.log(`Wrote ${audio.byteLength} bytes to speech.mp3`);For a multi-line script that returns a hosted URL plus subtitles instead of raw bytes, use speech({ scripts: [{ content, speakerId }] }) — it resolves to { audioUrl, audioDuration, subtitlesUrl, taskId, credits }.
Generate an image with a reference
createImage is synchronous and returns the result directly. Ground the generation on an existing image by passing referenceImages — either a hosted file (fileData) or inline base64 (inlineData). The response is an untyped object, so read fields defensively.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const result = await client.createImage({
provider: 'gemini',
prompt: 'Redraw this scene as a watercolor painting at golden hour',
referenceImages: [
{
fileData: {
fileUri: 'https://storage.googleapis.com/your-bucket/reference.png',
mimeType: 'image/png',
},
},
],
imageConfig: {
imageSize: '2K', // '1K' | '2K' | '4K'
aspectRatio: '16:9', // '16:9' | '4:3' | '1:1' | '3:4' | '9:16' | '21:9'
},
});
// createImage returns an untyped record — log it to see the shape, then read fields.
console.log(result);For inline image data, swap fileData for inlineData: { data: '<base64>', mimeType: 'image/png' }. Reference images steer composition and style; the prompt still drives the change you want.
Seedance 2.5
The existing methods also accept doubao-seedance-2-5. Use the 2.5 fields and rules below; legacy model restrictions remain unchanged.
Seedance 2.5 accepts up to 51 content items: one text, 30 images, 10 videos, and 10 audio files. Prompt limit: 5000 characters. Video and audio each require nested duration; audio-only input is supported. content is omitted for final generation from a draft.
Put metadata inside each image_url, video_url, or audio_url object alongside url. Each video and audio requires its own duration in seconds. Send actual width, height, size (bytes), and video fps when available; one aggregate duration cannot replace per-file metadata.
Create a playable draft with draft: true and resolution: "480p". Poll its task until success. Within 14 days of creation, request a new 1080p final using the ListenHub taskId as draftTaskId. It must be your own draft in the same application/API identity scope. The API restores its original inputs and channel; omit content and other generation settings from the final request.
Use a new requestId (8–128 characters) for each intended generation and reuse it for network retries of that same submission. A UUID is suitable. Task details expose optional draft, draftTaskId, and finalTaskIds; draftEpisodeId and finalEpisodeIds may be present for visible generated content.
Call POST /v1/video-generation/estimate-credits with the same model, resolution, ratio, duration, omniReferenceTaskType, and draft. Send referenceImages, referenceVideos, and referenceAudios as metadata arrays with role and each file’s metadata, without URLs. To estimate a final, send only model and draftTaskId.
Draft and final are separate tasks and separate charges. A final does not subtract the draft charge or treat the draft output as another reference video. A failed final refunds its own reserved charge and leaves the successful draft available.
OpenAPI estimates return tokens and estimatedCostUsd, not internal credits. Reserve the estimate at submission; actual-token settlement can refund or add the difference. Keep the returned task ID and check its state before retrying.
Full limits and runnable request shapes.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const draft = await client.createVideoGeneration({
model: 'doubao-seedance-2-5',
content: [{ type: 'text', text: 'A paper boat in a luminous city' }],
draft: true, resolution: '480p', ratio: '16:9', duration: 5,
requestId: 'draft-request-unique-id',
});
let detail = await client.getVideoGenerationTask(draft.taskId);
while (!['success', 'failed'].includes(detail.status)) {
await new Promise((resolve) => setTimeout(resolve, 5000));
detail = await client.getVideoGenerationTask(draft.taskId);
}
if (detail.status === 'success') {
const estimate = await client.estimateVideoCredits({
model: 'doubao-seedance-2-5', draftTaskId: draft.taskId,
});
console.log(estimate);
const final = await client.createVideoGeneration({
model: 'doubao-seedance-2-5', draftTaskId: draft.taskId,
requestId: 'final-request-unique-id',
});
console.log(final.taskId);
}Generate a video (SeeDance) and poll
createVideoGeneration runs the Doubao SeeDance models. Estimate the cost first with estimateVideoCredits, start the task, then poll getVideoGenerationTask until the status is success or failed. Build content from text plus optional reference frames.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
// Estimate before committing to an expensive job.
const estimate = await client.estimateVideoCredits({
model: 'doubao-seedance-2-fast',
resolution: '720p',
duration: 5,
});
console.log(estimate);
const task = await client.createVideoGeneration({
model: 'doubao-seedance-2-fast', // 'doubao-seedance-2-pro' | 'doubao-seedance-2-fast' | 'happyhorse'
content: [
{ type: 'text', text: 'A cat sprinting through a sunlit garden' },
{
type: 'image_url',
image_url: { url: 'https://example.com/cat.jpg' },
role: 'first_frame', // 'first_frame' | 'last_frame' | 'reference_image'
},
],
resolution: '720p', // '480p' | '720p' | '1080p'
duration: 5,
});
console.log(`Task created: ${task.taskId} (${task.status})`);
let detail = await client.getVideoGenerationTask(task.taskId);
while (detail.status !== 'success' && detail.status !== 'failed') {
await sleep(10_000);
detail = await client.getVideoGenerationTask(task.taskId);
console.log(`Status: ${detail.status}`);
}
if (detail.status === 'success') {
console.log(`Video: ${detail.videoUrl}`);
console.log(`Seed: ${detail.seed}`);
} else {
console.error('Video generation failed');
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}Generate music and poll
createMusicGenerate returns a task; poll getMusicTask until it leaves the in-progress states. A finished task carries a tracks array — each track has its own title and audioUrl.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const job = await client.createMusicGenerate({
prompt: 'Upbeat lo-fi hip hop beat with jazzy piano chords',
style: 'lo-fi',
title: 'Late Night Study',
});
console.log(`Music task: ${job.taskId} (${job.status})`);
let task = await client.getMusicTask(job.taskId);
while (task.status !== 'success' && task.status !== 'failed') {
await sleep(10_000);
task = await client.getMusicTask(job.taskId);
console.log(`Status: ${task.status}`);
}
if (task.status === 'success') {
for (const track of task.tracks) {
console.log(`${track.title} — ${track.audioUrl}`);
}
} else {
console.error(`Failed: ${task.errorMessage}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}Extract content from a URL
createContentExtract pulls clean text (and optional metadata) from a web page. It returns a taskId; poll getContentExtract until the status is completed or failed, then read data.content. Useful as the grounding step before a podcast or flow speech.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const { taskId } = await client.createContentExtract({
source: { type: 'url', uri: 'https://en.wikipedia.org/wiki/Mars' },
options: { summarize: true, maxLength: 2000 },
});
console.log(`Extract task: ${taskId}`);
let detail = await client.getContentExtract(taskId);
while (detail.status === 'processing') {
await sleep(3000);
detail = await client.getContentExtract(taskId);
console.log(`Status: ${detail.status}`);
}
if (detail.status === 'completed') {
console.log(detail.data?.content);
} else {
console.error(`Extract failed (failCode ${detail.failCode}): ${detail.message}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}Handle errors with ListenHubError
Every method throws a ListenHubError on a non-zero code or an HTTP error. Catch it to separate API failures from network or programming errors, and branch on status to react to auth and rate-limit cases. The requestId is what to quote when contacting support.
import { OpenAPIClient, ListenHubError } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
try {
// A bad ID surfaces as a structured API error.
const detail = await client.getPodcast('nonexistent-id');
console.log(detail.title);
} catch (err) {
if (err instanceof ListenHubError) {
console.error(`[${err.status}] ${err.code}: ${err.message}`);
if (err.requestId) console.error(`request ${err.requestId}`);
if (err.status === 401 || err.status === 403) {
// Credential rejected — rotate the API key.
} else if (err.status === 429) {
// The client already retried up to maxRetries; you are still limited.
}
} else {
// Network failure, timeout, or a bug — not an API error.
throw err;
}
}A 429 Too Many Requests is retried automatically (up to maxRetries, default 2) using the Retry-After header before it ever reaches your catch. See Configuration.
Check cost before you generate
Credit costs vary by product, length, and options, so these recipes do not quote numbers. Read your live balance with getSubscription() (the totalAvailableCredits field), and call the relevant estimate endpoint — for example estimateVideoCredits — before an expensive job. See the OpenAPI reference for which products expose an estimate endpoint.
Next steps
Quickstart
Install, authenticate, and create your first episode end to end.
Configuration
Base URL, timeout, retries, response unwrapping, and the client.api escape hatch.
Authentication
API keys vs. OAuth tokens, the accessToken getter, and where credentials live.
OpenAPI reference
Endpoint-level detail: paths, parameters, enums, and response shapes.