ListenHubSDKs & CLI
JavaScript SDK

SDK Reference

Every method on OpenAPIClient and ListenHubClient, grouped by product, with signatures, endpoints, and what each returns.

This is the complete method reference for @marswave/listenhub-sdk. Methods are grouped by product. Each entry lists the signature, one line on what it does, and the underlying HTTP endpoint.

The SDK ships two clients. They share response handling (unwrap data on code 0, throw ListenHubError otherwise, auto-retry 429) but target different API surfaces and authenticate differently:

OpenAPIClientListenHubClient
AuthAPI key (Authorization: Bearer)OAuth user access token
Base URLhttps://api.marswave.ai/openapihttps://api.listenhub.ai/api
Acts asYour account / key ownerThe signed-in user
Use forServers, scripts, CIUser-facing apps

OpenAPIClient is the public OpenAPI product — that surface is the focus of this reference. ListenHubClient is the OAuth client used by first-party apps; its methods are listed under ListenHubClient methods at the end.

Generation is asynchronous. A create* call returns an id immediately; poll the matching get* method until processStatus (or task status) leaves pending / generating. See Quickstart for the full loop.

Conventions used below:

  • "Endpoint" paths are relative to the client base URL. For OpenAPIClient that is https://api.marswave.ai/openapi/.
  • Methods return the unwrapped data payload. The { code, message, data } envelope is handled for you.
  • A few methods return a raw Response (binary or streaming) — these are called out explicitly.
  • Never hard-code credit costs. Use the estimate*Credits methods and getSubscription() instead.

OpenAPIClient methods

Speakers

A voice is identified by its speakerId. List voices before creating any episode and pass the ids you want.

MethodEndpointReturnsNotes
listSpeakers(params?)GET v1/speakers/list{ items: OpenAPISpeaker[] }Available voices

listSpeakers parameters:

ParamTypeNotes
languagestringFilter by language, e.g. en, zh, ja
statusnumberAvailability filter

Each OpenAPISpeaker carries speakerId, name, gender, language, demoAudioUrl, and an optional profile (pitch, speed, traits, styles, scenes, accent, description).

const { items } = await client.listSpeakers({ language: 'en' });
const speakerId = items[0].speakerId;

Podcast

A podcast is a multi-speaker conversation generated from a query and/or sources. Create it, then poll getPodcast until processStatus is success.

MethodEndpointReturns
createPodcast(params)POST v1/podcast/episodes{ episodeId }
getPodcast(episodeId)GET v1/podcast/episodes/{episodeId}OpenAPIPodcastDetail
createPodcastTextContent(params)POST v1/podcast/episodes/text-content{ episodeId, message }
generatePodcastAudio(episodeId, params?)POST v1/podcast/episodes/{episodeId}/audio{ success, message, episodeId, status }
getPodcastTextStream(episodeId, event)GET v1/podcast/episodes/{episodeId}/text-streamraw Response (stream)

createPodcast parameters (OpenAPICreatePodcastParams):

ParamTypeNotes
querystringWhat the episode should cover
sourcesArray<{ type: 'text' | 'url'; content: string }>Grounding material; raw text or a page URL
speakersArray<{ speakerId: string }>Required; one entry per voice
languagestringOutput language
modestringGeneration depth (e.g. quick, deep)

The two-step text-then-audio flow lets you review or edit the script before paying for audio: createPodcastTextContent generates the script only, then generatePodcastAudio(episodeId, { scripts }) renders audio from scripts you optionally rewrite. getPodcastTextStream(episodeId, 'script' | 'outline') returns a streaming Response for live script/outline tokens.

OpenAPIPodcastDetail includes processStatus, title, outline, cover, audioUrl, audioStreamUrl, subtitlesUrl, scripts (per-speaker lines), credits, and failCode on failure.

Flow Speech / TTS

Flow speech turns text or a URL into narrated audio with one or more voices. TTS endpoints are lower-level: they synthesize speech from explicit scripts.

Flow speech

MethodEndpointReturns
createFlowSpeech(params)POST v1/flow-speech/episodes{ episodeId }
getFlowSpeech(episodeId)GET v1/flow-speech/episodes/{episodeId}OpenAPIFlowSpeechDetail
createFlowSpeechTTS(params)POST v1/flow-speech/episodes/tts{ episodeId }
getFlowSpeechTextStream(episodeId, event)GET v1/flow-speech/episodes/{episodeId}/text-streamraw Response (stream)

createFlowSpeech parameters (OpenAPICreateFlowSpeechParams):

ParamTypeNotes
sourcesArray<{ type: 'text' | 'url'; content?: string; uri?: string }>Required; content for text, uri for URL
speakersArray<{ speakerId: string }>Required
languagestringOutput language
mode'smart' | 'direct'smart rewrites for narration; direct reads as-is

createFlowSpeechTTS takes scripts: Array<{ content: string; speakerId: string }> plus an optional title, and renders the lines verbatim. event for the text stream is 'script' | 'outline'.

OpenAPIFlowSpeechDetail includes processStatus, title, outline, cover, audioUrl, audioStreamUrl, subtitlesUrl, scripts, and sourceProcessResult.

TTS / speech

MethodEndpointReturns
speech(params)POST v1/speechOpenAPISpeechResponse
tts(params)POST v1/ttsraw Response (audio bytes)
audioSpeech(params)POST v1/audio/speechraw Response (audio bytes)

speech takes scripts: Array<{ content: string; speakerId: string }> and returns a synchronous result: { audioUrl, audioDuration, subtitlesUrl?, taskId, credits }.

tts and audioSpeech are OpenAI-compatible single-voice synthesis. They take { input, voice, response_format? } where response_format is one of mp3 (default), opus, aac, flac, wav, pcm, and return the audio as a raw Response — read it with .arrayBuffer() or stream .body.

const res = await client.tts({ input: 'Hello world', voice: speakerId, response_format: 'mp3' });
const audio = Buffer.from(await res.arrayBuffer());

Storybook (explainer / slides)

Storybook produces page-based visual content — explainer videos and slide decks. mode selects the format. Audio is optional via skipAudio.

MethodEndpointReturns
createStorybook(params)POST v1/storybook/episodes{ episodeId }
getStorybook(episodeId)GET v1/storybook/episodes/{episodeId}OpenAPIStorybookDetail
generateStorybookVideo(episodeId)POST v1/storybook/episodes/{episodeId}/video{ success }

createStorybook parameters (OpenAPICreateStorybookParams):

ParamTypeNotes
sourcesArray<{ type: 'text' | 'url'; content: string }>Required grounding material
speakersArray<{ speakerId: string }>Optional; needed only when generating audio
mode'info' | 'story' | 'slides'slides for a deck; info / story for explainers
skipAudiobooleanVisual-only output when true
stylestringVisual style hint
languagestringOutput language

After an episode succeeds, generateStorybookVideo(episodeId) renders a downloadable video from the pages. Poll getStorybook and watch videoStatus (not_generatedpendingsuccess / fail); videoUrl appears on success.

OpenAPIStorybookDetail includes mode, processStatus, title, cover, audioUrl, audioDuration, videoUrl, videoStatus, and pages (each with text, pageNumber, imageUrl, audioTimestamp).

Image

Single-call image generation. There is no separate poll method on OpenAPIClient — the response carries the result.

MethodEndpointReturns
createImage(params)POST v1/images/generationOpenAPICreateImageResponse

createImage parameters (OpenAPICreateImageParams):

ParamTypeNotes
providerstringRequired image provider
modelstringProvider model
promptstringRequired text prompt
referenceImagesArray<{ fileData?; inlineData? }>Reference images — fileData: { fileUri, mimeType } or inlineData: { data, mimeType } (base64)
imageConfig{ imageSize?; aspectRatio? }imageSize: 1K | 2K | 4K; aspectRatio: 16:9 | 4:3 | 1:1 | 3:4 | 9:16 | 21:9

Video (SeeDance / HappyHorse + PixVerse)

Two video families share the same poll/list/estimate methods but have different create methods and parameters. SeeDance (doubao-seedance-2-*) and HappyHorse use a content array; PixVerse uses a capability-based shape.

SeeDance / HappyHorse

MethodEndpointReturns
createVideoGeneration(params)POST v1/video-generation/generate{ taskId, status }
getVideoGenerationTask(taskId)GET v1/video-generation/tasks/{taskId}OpenAPIVideoGenerationTaskDetail
listVideoGenerationTasks(params?)GET v1/video-generation/tasks{ items, page, pageSize, total }
estimateVideoCredits(params)POST v1/video-generation/estimate-credits{ tokens, credits }

createVideoGeneration parameters (OpenAPICreateVideoGenerationParams):

ParamTypeNotes
model'doubao-seedance-2-pro' | 'doubao-seedance-2-fast' | 'happyhorse'Defaults to a SeeDance model
contentVideoContentItem[]Required; mixed text / image / video / audio items (see below)
resolution'480p' | '720p' | '1080p'1080p only on doubao-seedance-2-pro; happyhorse has no 480p
ratio'16:9' | '4:3' | '1:1' | '3:4' | '9:16' | '21:9' | '4:5' | '5:4'4:5 / 5:4 only on happyhorse
durationnumberSeconds; SeeDance min 4, HappyHorse min 3
generateAudiobooleanGenerate an audio track
seednumberReproducibility seed
inputVideoDurationnumberFor video-edit input; SeeDance [2,15], HappyHorse [3,60]
audioSetting'auto' | 'origin'HappyHorse video-edit only, when content includes a video_url

Each content item is one of:

  • { type: 'text', text }
  • { type: 'image_url', image_url: { url }, role: 'first_frame' \| 'last_frame' \| 'reference_image' }
  • { type: 'video_url', video_url: { url }, role: 'reference_video' }
  • { type: 'audio_url', audio_url: { url }, role: 'reference_audio' }

HappyHorse rejects last_frame and audio_url content. estimateVideoCredits takes { model, resolution, duration, hasVideoInput?, inputVideoDuration?, ratio? }.

const { taskId } = await client.createVideoGeneration({
  model: 'doubao-seedance-2-pro',
  content: [{ type: 'text', text: 'A timelapse of a city at dusk' }],
  resolution: '1080p',
  duration: 5,
});

PixVerse

MethodEndpointReturns
createPixVerseVideoGeneration(params)POST v1/video-generation/pixverse/generate{ taskId, episodeId?, status }
estimatePixVerseVideoCredits(params)POST v1/video-generation/pixverse/estimate-credits{ tokens, credits }

Poll and list PixVerse tasks with the same getVideoGenerationTask / listVideoGenerationTasks methods above.

createPixVerseVideoGeneration parameters (OpenAPICreatePixVerseVideoParams):

ParamTypeNotes
capability'text_to_video' | 'image_to_video' | 'transition' | 'multi_transition' | 'fusion' | 'restyle' | 'mimic' | 'lip_sync' | 'agent'Required; selects the generation mode
model'pixverse' | 'v6' | 'v5' | 'v4.5'PixVerse model version
language'zh' | 'en'Service region; en international, zh mainland China
promptstringText prompt
durationnumberSeconds (1–60; agent: 20/30/60)
aspectRatio'9:16' | '16:9' | '1:1' | '4:3' | '3:4'Output aspect ratio
quality'360p' | '540p' | '720p' | '1080p'mimic locked to 720p; agent needs 720p/1080p
sourceTaskIdstringReuse a prior succeeded task (restyle / lip_sync)
images / videos / audiosArray<{ url; duration? }>Input assets
pixverseOpenAPIPixVerseOptionsCapability-specific nested options

The nested pixverse object covers agentType, motionMode, cameraMovement, templateId, multiTransition, imageReferences, tts, soundEffect*, lipSyncTts*, brandSticker, and introOutroClip. Relevance depends on capability. estimatePixVerseVideoCredits takes { capability, model?, language?, duration?, quality?, pixverse? } with a reduced pixverse shape for the estimate.

Music

Music endpoints default to the Mureka provider. Async endpoints return a task ({ taskId, taskType, status }); poll getMusicTask. Synchronous endpoints (recognizeMusic, describeMusic, stemMusic) return their result directly.

MethodEndpointReturnsSync?
createMusicGenerate(params)POST v1/music/generateCreateMusicTaskResponseasync
createMusicCover(params)POST v1/music/coverCreateMusicTaskResponseasync (deprecated)
createMusicExtend(params)POST v1/music/extendCreateMusicTaskResponseasync
createMusicRemix(params)POST v1/music/remixCreateMusicTaskResponseasync
createMusicInstrumental(params)POST v1/music/instrumentalCreateMusicTaskResponseasync
createMusicSoundtrack(params)POST v1/music/soundtrackCreateMusicTaskResponseasync
createMusicTrack(params)POST v1/music/trackCreateMusicTaskResponseasync
recognizeMusic(params)POST v1/music/recognizeRecognizeMusicResponsesync
describeMusic(params)POST v1/music/describeDescribeMusicResponsesync
stemMusic(params)POST v1/music/stemStemMusicResponsesync
getMusicTask(taskId)GET v1/music/tasks/{taskId}MusicTaskDetail
listMusicTasks(params?)GET v1/music/tasks{ items, page, pageSize, total }

Key create parameters:

  • createMusicGenerate: { prompt?, lyrics?, model?, style?, title?, instrumental?, vocalId? }. model is one of auto, mureka-7.6, mureka-8, mureka-9, mureka-o2.
  • createMusicExtend: { uploadUrl, model, continueAt, prompt?, style?, title?, instrumental?, negativeTags?, vocalGender?, styleWeight?, weirdnessConstraint?, audioWeight? }. model here is a Suno version (V4, V4_5, V4_5PLUS, V4_5ALL, V5, V5_5).
  • createMusicRemix: { audio?, audioFilename?, audioUrl?, providerSongId?, lyrics, prompt }. Provide exactly one of audio / audioUrl / providerSongId. Prefer this over the deprecated createMusicCover.
  • createMusicInstrumental: { prompt?, referenceAudio?, referenceAudioFilename?, model? }. Provide prompt XOR referenceAudio.
  • createMusicSoundtrack: { image?, video?, prompt?, model? }. Provide image XOR video.
  • createMusicTrack: { audio?, providerSongId?, generateType, prompt, lyrics?, vocalGender?, generateStart?, generateEnd? }. generateType selects the stem (Vocals, Instrumental, Drums, …); lyrics is required when generateType is Vocals.

The multipart endpoints (remix, instrumental, soundtrack, track, recognize, describe, stem) take a Blob/File for the audio/image/video field. In Node 20+, wrap a buffer with new Blob([buffer]).

const { taskId } = await client.createMusicGenerate({
  prompt: 'lo-fi hip hop, mellow, rainy night',
  model: 'auto',
});
let task = await client.getMusicTask(taskId);
while (task.status === 'pending' || task.status === 'generating') {
  await sleep(10_000);
  task = await client.getMusicTask(taskId);
}
console.log(task.tracks[0]?.audioUrl);

Synchronous results: recognizeMusic returns timestamped lyricsSections; describeMusic returns { description, tags, genres, instruments }; stemMusic returns { zipUrl, midiZipUrl, expiresAt } (links expire ~24h).

Content Extract

Extract readable content from a URL (article text, optional summary). Asynchronous: create, then poll.

MethodEndpointReturns
createContentExtract(params)POST v1/content/extract{ taskId }
getContentExtract(taskId)GET v1/content/extract/{taskId}OpenAPIContentExtractDetail

createContentExtract parameters (OpenAPICreateContentExtractParams):

ParamTypeNotes
source{ type: 'url'; uri: string }Required; the page to extract
options{ summarize?; maxLength?; twitter? }summarize adds a summary; twitter: { count? } for thread depth

Poll getContentExtract until status is completed. The detail carries data.content, data.metadata, data.references, and credits.

Subscription

MethodEndpointReturns
getSubscription()GET v1/user/subscriptionOpenAPISubscriptionInfo

Returns credit balance and plan info: totalAvailableCredits, the monthly/permanent/limited-time credit breakdown, resetAt, renewStatus, paidStatus, and subscriptionPlan. Use totalAvailableCredits to check your balance before an expensive job.

Files

OpenAPIClient has no dedicated file-upload method. To use a local file as input, host it at a public URL and pass that URL (for example as a source uri, a referenceImages.fileData.fileUri, or a video image_url.url). The presigned-upload flow lives on ListenHubClient — see Files below.


ListenHubClient methods

ListenHubClient authenticates with an OAuth user access token and targets https://api.listenhub.ai/api. It exposes the first-party app surface. The product shapes differ from OpenAPIClient: episodes use a nested template object, and creation methods are split per product (podcast, TTS, explainer, slides). Use this client only when each request runs under an individual signed-in user.

Auth & session

MethodEndpointReturns
connectInit(params)POST v1/auth/connect/init{ sessionId, authUrl }
connectToken(params)POST v1/auth/connect/token{ accessToken, refreshToken, expiresIn }
refresh(params)POST v1/auth/token{ accessToken, refreshToken, expiresIn }
revoke(params)POST v1/auth/token/revokevoid

connectInit({ callbackPort }) starts the device/OAuth flow and returns an authUrl to open; connectToken({ sessionId, code }) exchanges the result for tokens. refresh({ refreshToken }) rotates an expiring access token; revoke({ refreshToken }) invalidates it.

Speakers

MethodEndpointReturns
listSpeakers(params?)GET v1/settings/speakers{ items: Speaker[] }

Parameters: { language?, status? }. Note the return shape differs from OpenAPIClient — each Speaker exposes speakerInnerId (not speakerId), plus personality, accessType, and weight. The speakerInnerId is what the episode template.speakers array expects.

Podcast

MethodEndpointReturns
createPodcast(params)POST v1/episodes/all-in-one{ episodeId }
listPodcasts(params?)GET v1/episodes (productId=aiPodcast)ListEpisodesResponse

createPodcast parameters (CreatePodcastParams):

ParamTypeNotes
type'podcast-solo' | 'podcast-duo'Single or two-host
querystringTopic
sourcesContentSource[]{ type: 'url' | 'text'; uri?; content? }
template{ type: 'podcast'; mode; speakers; language }mode: quick | deep | debate; speakers: speakerInnerId[]; language: en | zh | ja

Flow Speech / TTS

MethodEndpointReturns
createTTS(params)POST v1/episodes/flow-speech{ episodeId }
listTTS(params?)GET v1/episodes (productId=textToSpeech)ListEpisodesResponse

createTTS parameters (CreateTTSParams): { sources, template: { type: 'flowspeech', mode: 'smart' | 'direct', speakers, language } }.

Storybook (explainer / slides)

MethodEndpointReturns
createExplainerVideo(params)POST v1/episodes/storybook{ episodeId }
createSlides(params)POST v1/episodes/storybook (mode: 'slides', skipAudio: true){ episodeId }
exportExplainerVideo(episodeId)POST v1/episodes/{episodeId}/storybook/videovoid
listExplainerVideos(params?)GET v1/episodes (productId=explainerVideo)ListEpisodesResponse
listSlides(params?)GET v1/episodes (productId=slideDeck)ListEpisodesResponse

createExplainerVideo and createSlides share a shape: { query?, sources?, style?, styleOverride?, skipAudio?, imageConfig?, template }. The template carries type: 'storybook', mode (info / story for explainers, slides for decks), speakers, language, and optional style, size (2K / 4K), aspectRatio (16:9 / 9:16 / 1:1), pageCount. createSlides defaults skipAudio to true and fixes mode to slides. exportExplainerVideo triggers the downloadable video render.

Episodes (shared)

These work across all four products on ListenHubClient.

MethodEndpointReturns
getCreation(episodeId)GET v5/episodes/{episodeId}/detailEpisodeDetail
deleteCreations(params)DELETE v1/episodesvoid

getCreation returns the full EpisodeDetail (status, speakers, topicDetail with title/outline/audio/video/pages/scripts). deleteCreations({ ids }) batch-soft-deletes up to 100 episodes by id; passing a video episode id also soft-deletes its video task and refunds credits for any in-progress task. The per-product list* methods all hit GET v1/episodes with a productId filter and accept { page?, pageSize? }.

Image

MethodEndpointReturns
createAIImage(params)POST v1/images{ imageId }
getAIImage(imageId)GET v1/images/{imageId}AIImageItem
listAIImages(params?)GET v1/images{ items, pagination }
deleteAIImages(params)DELETE v1/imagesvoid

createAIImage parameters (CreateAIImageParams):

ParamTypeNotes
promptstringRequired
referenceImageUrlsstring[]Reference image URLs
language'auto' | 'en' | 'ja' | 'ko' | 'hi' | 'zh' | 'pt' | 'es'Prompt language
aspectRatio'1:1' | '2:3' | '3:2' | '3:4' | '4:3' | '9:16' | '16:9' | '21:9' | …Output ratio
imageSize'1K' | '2K' | '4K'Output resolution
model'gemini-3-pro-image' | 'gemini-3.1-flash-image'Image model
isLosslessbooleanLossless encoding
enableSearchbooleanAllow web search for grounding

Generation is async — poll getAIImage(imageId) until status is terminal and imageUrl is set. deleteAIImages({ ids }) batch-soft-deletes up to 100 images (owner-scoped; unknown ids ignored).

Music

ListenHubClient exposes the same music method set as OpenAPIClient (same endpoints, same parameters): createMusicGenerate, createMusicCover, createMusicExtend, createMusicRemix, createMusicInstrumental, createMusicSoundtrack, createMusicTrack, recognizeMusic, describeMusic, stemMusic, getMusicTask, listMusicTasks. See Music above for parameters and the poll loop.

Lyrics

MethodEndpointReturns
createLyrics(params)POST v1/lyrics/generate{ taskId, status }
getLyricsTask(taskId)GET v1/lyrics/tasks/{taskId}LyricsTaskDetail
listLyricsTasks(params?)GET v1/lyrics/tasks{ items, page, pageSize, total }

createLyrics({ prompt }) starts an async lyrics task; poll getLyricsTask until status is success, then read variants (each { text, title, status }).

Video Generation

ListenHubClient exposes the same video methods as OpenAPIClient, with one naming difference: the SeeDance/HappyHorse estimate method is estimateVideoGenerationCredits (vs. estimateVideoCredits on OpenAPIClient).

MethodEndpointReturns
createVideoGeneration(params)POST v1/video-generation/generate{ taskId, status }
getVideoGenerationTask(taskId)GET v1/video-generation/tasks/{taskId}VideoGenerationTaskDetail
listVideoGenerationTasks(params?)GET v1/video-generation/tasks{ items, page, pageSize, total }
estimateVideoGenerationCredits(params)POST v1/video-generation/estimate-credits{ tokens, credits }
createPixVerseVideoGeneration(params)POST v1/video-generation/pixverse/generate{ taskId, episodeId?, status }
estimatePixVerseVideoCredits(params)POST v1/video-generation/pixverse/estimate-credits{ tokens, credits }

Parameters match the Video section above.

Subscription & user

MethodEndpointReturns
getCurrentUser()GET v1/users/meUserProfile
getSubscription()GET v1/users/subscriptionSubscriptionInfo
getSettings()GET v2/settingsSettingsResponse

Note the endpoint differs from OpenAPIClient (v1/users/subscription vs. v1/user/subscription). getSettings returns the user's saved per-product defaults (speakers, language, duration, mode, style images).

Checkin

MethodEndpointReturns
checkinSubmit()POST v1/checkin{ checkinDate, rewardCredits }
checkinStatus()GET v1/checkin/statusCheckinStatusResponse

Daily check-in for reward credits. checkinStatus reports hasCheckedInToday, lastCheckinTime, and monthlyCheckinCount.

Settings / API key

MethodEndpointReturns
getApiKey()GET v1/settings/api-key{ key }
regenerateApiKey()POST v1/settings/api-key/regenerate{ key }

Lets a signed-in user read or rotate their OpenAPI key. regenerateApiKey invalidates the previous key.

Files

MethodEndpointReturns
createFileUpload(params)POST v1/files{ presignedUrl, fileUrl }
getFileDownloadUrl(fileUrl)GET v1/files{ downloadUrl }

createFileUpload({ fileKey, contentType, category }) returns a presignedUrl to PUT the bytes to, plus the fileUrl to reference afterward. getFileDownloadUrl(fileUrl) resigns a stored file URL into a time-limited downloadUrl.


The client.api escape hatch

ListenHubClient exposes its underlying ky instance as client.api for endpoints the SDK does not wrap. It applies the same auth, base URL, and retry behavior, but you parse the response yourself. Paths are relative to the base URL — no leading / (a ky requirement):

const me = await client.api.get('v1/users/me').json();

OpenAPIClient does not expose client.api. For an OpenAPI endpoint the SDK does not cover yet, call it with your own HTTP client using the same Authorization: Bearer header — see the OpenAPI reference.

Errors

Every method throws a ListenHubError on a non-zero code or an HTTP error. It carries status, code, and requestId — include requestId when reporting an issue.

import { ListenHubError } from '@marswave/listenhub-sdk';

try {
  await client.getPodcast('nope');
} catch (err) {
  if (err instanceof ListenHubError) {
    console.error(`[${err.status}] code ${err.code} (request ${err.requestId})`);
  } else {
    throw err;
  }
}

Next steps

On this page