Configuration
Configure base URL, timeout, and retries for both SDK clients, and learn how response unwrapping, ListenHubError, and 429 auto-retry behave.
Both SDK clients take an options object in their constructor. The two share the same option names and the same response handling, but they default differently — OpenAPIClient is built for server-side work and uses a longer timeout. This page covers every option, the response envelope the clients unwrap for you, how errors surface as ListenHubError, the automatic 429 retry, and the client.api escape hatch for endpoints the SDK has not wrapped yet.
Options
ListenHubClient accepts ClientOptions; OpenAPIClient accepts OpenAPIClientOptions. Every field is optional.
| Option | Type | OpenAPIClient default | ListenHubClient default |
|---|---|---|---|
apiKey | string | LISTENHUB_API_KEY env var | — (not applicable) |
accessToken | string | (() => string | undefined) | — (not applicable) | none (anonymous) |
baseURL | string | https://api.marswave.ai/openapi | https://api.listenhub.ai/api |
timeout | number (ms) | 60000 | 30000 |
maxRetries | number | 2 | 2 |
The credential differs by client: OpenAPIClient authenticates with apiKey, ListenHubClient with accessToken. See Authentication for how to obtain each.
baseURL
Points the client at a specific API host. The defaults target production, so you rarely set this by hand. Each client also reads an environment variable as a fallback when you do not pass baseURL:
OpenAPIClient→LISTENHUB_OPENAPI_URLListenHubClient→LISTENHUB_API_URL
An explicit baseURL option always wins over the environment variable.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient({
baseURL: 'https://api.marswave.ai/openapi',
});accessToken (ListenHubClient)
accessToken accepts a static string or a getter function () => string | undefined. When you pass a getter, the SDK calls it before every request, so a long-lived client always sends your current token without being recreated. This is the recommended shape when tokens refresh over time.
import { ListenHubClient } from '@marswave/listenhub-sdk';
// Static token — fine for short-lived clients
const client = new ListenHubClient({ accessToken: tokens.accessToken });
// Getter — called before each request; return the freshest token you hold
const live = new ListenHubClient({ accessToken: () => store.accessToken });The token refresh lifecycle (when and how to mint a new token) lives in Authentication.
apiKey (OpenAPIClient)
apiKey is the credential for server-side work. Pass it explicitly, or omit it and let the constructor read LISTENHUB_API_KEY from the environment. If neither is present, the constructor throws.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient({ apiKey: process.env.LISTENHUB_API_KEY });An API key is a secret with full access to your account. Keep it server-side — never ship it in browser or mobile code. For user-facing apps, use ListenHubClient with OAuth so each request runs under the user's own account.
timeout
Per-request timeout in milliseconds. A request that takes longer is aborted and rejects. OpenAPIClient defaults to 60000 (60s) because server-side generation calls can run long; ListenHubClient defaults to 30000 (30s).
const client = new OpenAPIClient({ timeout: 120_000 }); // 2 minutesThe timeout applies to the HTTP request itself, not to long-running generation jobs. Generation is asynchronous — a create call returns an episodeId or taskId quickly, and you poll for the result. See the Quickstart create-and-poll loop.
maxRetries
How many times the client retries a 429 Too Many Requests response before giving up. Defaults to 2. Set it to 0 to disable retries. Only 429 is retried — other error statuses throw immediately. See 429 rate-limit retry below.
const client = new OpenAPIClient({ maxRetries: 0 }); // fail fast, no retryResponse unwrapping
Every ListenHub response is wrapped in an envelope:
{ "code": 0, "message": "", "data": { } }A code of 0 means success; any non-zero code is an error. Both clients handle the envelope for you in an afterResponse hook, so your code works with data directly:
- On
code 0, the method resolves todata— you never see the wrapper. - On a non-zero
code, the method throws aListenHubErrorcarrying the envelope'scode,message, andrequest_id. - A
204 No Contentresponse (and any non-JSON body) passes through untouched.
// The SDK returns data directly — no envelope to peel
const { items } = await client.listSpeakers({ language: 'en' });A few methods return the raw Response instead of unwrapped JSON because they stream or return binary — for example TTS audio (tts, audioSpeech) and the text-stream endpoints. Those are the exception; the rest resolve to data.
Error handling
When a request fails — a rejected credential, a validation error, a non-zero code, or an HTTP error status — the method throws a ListenHubError. Catch it and inspect its fields:
| Field | Type | Meaning |
|---|---|---|
status | number | HTTP status code (e.g. 401, 429, 500) |
code | string | Backend error code from the envelope, or GATEWAY_ERROR / UNKNOWN_ERROR for non-JSON failures |
message | string | Human-readable error message |
requestId | string | undefined | Request identifier — include it when contacting support |
import { OpenAPIClient, ListenHubError } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
try {
const sub = await client.getSubscription();
console.log(sub);
} catch (err) {
if (err instanceof ListenHubError) {
// Structured error from the API
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, or refresh the OAuth token
}
} else {
// Network failure, timeout, or other non-API error
throw err;
}
}ListenHubError is also thrown for non-JSON failures. A gateway or proxy returning an HTML error page surfaces as code: 'GATEWAY_ERROR' (with the page <title> as the message); anything else unparseable surfaces as code: 'UNKNOWN_ERROR'. In both cases status still reflects the HTTP status.
429 rate-limit retry
A 429 Too Many Requests response is retried automatically — you do not write retry logic. The client:
- Reads the
Retry-Afterheader to decide how long to wait. - Retries up to
maxRetriestimes (default2). - If every retry is exhausted, throws a
ListenHubErrorwithstatus: 429.
Because of this, a brief rate limit usually resolves itself and never surfaces as an error. Set maxRetries: 0 if you want 429 to throw immediately instead. Only 429 triggers this path; all other error statuses throw on the first response.
The client.api escape hatch
ListenHubClient exposes its underlying ky instance as client.api. Use it to call an endpoint the SDK has not wrapped yet — the same auth header, base URL, envelope unwrapping, and 429 retry still apply, because you are reusing the configured client.
import { ListenHubClient } from '@marswave/listenhub-sdk';
const client = new ListenHubClient({ accessToken: tokens.accessToken });
// Paths are relative to baseURL — no leading slash (ky requirement)
const data = await client.api.get('v1/some/new-endpoint').json();
const created = await client.api
.post('v1/some/new-endpoint', { json: { foo: 'bar' } })
.json();Paths passed to client.api are relative to baseURL and must not start with a leading / — that is a ky requirement. Use v1/..., not /v1/....
The escape hatch is available on ListenHubClient. On OpenAPIClient, prefer the typed methods; if you need an endpoint it does not cover, reach for the OpenAPI reference and call it with your own HTTP client using the same Authorization: Bearer header.