ListenHubSDKs & CLI
JavaScript SDK

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() reads LISTENHUB_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 episodeId or taskId immediately; you poll a get* method until the status leaves pending / generating. Every recipe below uses the same sleep helper 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.

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(`Estimated credits: ${estimate.credits}`);

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

On this page