SDKs
Official TypeScript and Python clients with typed methods, auto-pagination, retries, idempotency, webhook verification and OAuth helpers.
2 min read
Publishing soon
The SDKs are built and tested, but not on npm or PyPI yet. The install commands below will work once they are published, which the changelog will announce. Until then, call the API directly; every page shows fetch and requests examples.
| Language | Package | Install |
|---|---|---|
| TypeScript and JavaScript | @listener/api | npm install @listener/api |
| Python 3.9+ | listener-api | pip install listener-api |
Both SDKs are generated from the same OpenAPI document as this reference, so every endpoint has a method and every response field keeps the API’s own snake_case name. The reference shows each operation’s SDK method next to its path.
- Zero-dependency TypeScript client on the platform
fetch(Node 18+, Deno, Bun, Workers, edge runtimes, browsers for OAuth public clients). Sync and async Python clients onhttpx. - Cursor auto-pagination, typed errors that mirror the error object, and retries on
429,5xxand network errors. - An
Idempotency-Keyon every write, reused across retries, so a retried write is applied at most once. dryRun/dry_runon every write andconfirmonly on the methods that need it.Listener-Version: 2026-10-14pinned on every request.
Quickstart#
Both clients read LISTENER_API_KEY from the environment when you do not pass a key.
import Listener from '@listener/api';
const listener = new Listener({ apiKey: process.env.LISTENER_API_KEY });
const me = await listener.me.retrieve();
console.log(me.network_name, me.kind);
// Every show, fetching pages as needed
for await (const show of listener.shows.list()) {
console.log(show.id, show.name, show.connected_platforms);
}
const reach = await listener.analytics.reach({ time_range: '30d' });
console.log(reach.total.current, reach.total.growth);Naming#
Methods follow the API’s resources. Path parameters are positional; query and body parameters go in one object (TypeScript) or as keyword arguments (Python), with the API’s names.
| Endpoint | TypeScript | Python |
|---|---|---|
GET /v1/campaigns | listener.campaigns.list() | client.campaigns.list() |
POST /v1/campaigns | listener.campaigns.create({…}) | client.campaigns.create(…) |
POST /v1/shows/{show_id}/clusters | listener.shows.clusters.create(showId, {…}) | client.shows.clusters.create(show_id, …) |
PUT /v1/campaigns/{id}/slots/{slot_id}/cluster | listener.campaigns.slots.linkCluster(id, slotId, {…}) | client.campaigns.slots.link_cluster(id, slot_id, …) |
POST /v1/webhook-endpoints | listener.webhookEndpoints.create({…}) | client.webhook_endpoints.create(…) |
Dry runs, confirmations and errors#
import { ConflictError, PermissionError } from '@listener/api';
const draft = { title: 'Acme Coffee Q4', start_date: '2026-11-01', end_date: '2026-12-31', brand: { name: 'Acme Coffee' } };
const preview = await listener.campaigns.create(draft, { dryRun: true });
console.log(preview.would_write);
try {
await listener.campaigns.delete(campaignId);
} catch (err) {
if (err instanceof ConflictError && err.code === 'confirmation_required') {
await listener.campaigns.delete(campaignId, { confirm: true });
} else if (err instanceof PermissionError) {
console.error(`This key needs ${err.requiredScope} (request ${err.requestId})`);
} else throw err;
}Webhooks#
The SDKs verify the Listener-Signature header exactly as described in Verify signatures, including both signatures during a secret rotation and a 300-second tolerance. Pass the raw body.
// Next.js route handler: app/api/webhooks/listener/route.ts
import { webhooks } from '@listener/api';
export async function POST(request: Request) {
const body = await request.text();
try {
const event = webhooks.constructEvent(body, request.headers.get('listener-signature'), process.env.LISTENER_WEBHOOK_SECRET!);
// handle event.type …
return new Response(null, { status: 200 });
} catch {
return new Response('invalid signature', { status: 400 });
}
}OAuth#
Helpers cover PKCE, the authorization URL, the code exchange and refresh-token rotation. Refreshes are single-flight, so concurrent requests never reuse a rotated refresh token.
import { Listener, createPkcePair, createState } from '@listener/api';
const listener = new Listener();
const pkce = await createPkcePair();
const state = await createState();
const url = listener.oauth.buildAuthorizeUrl({
clientId: process.env.LISTENER_CLIENT_ID!,
redirectUri: 'https://yourapp.com/oauth/callback',
scope: ['analytics:read', 'shows:read', 'offline_access'],
state,
codeChallenge: pkce.codeChallenge,
});
// In the callback, after checking state:
const tokens = await listener.oauth.exchangeCode({
code,
redirectUri: 'https://yourapp.com/oauth/callback',
codeVerifier: pkce.codeVerifier,
clientId: process.env.LISTENER_CLIENT_ID!,
clientSecret: process.env.LISTENER_CLIENT_SECRET,
});
const userClient = new Listener({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
accessTokenExpiresAt: tokens.expiresAt,
clientId: process.env.LISTENER_CLIENT_ID!,
clientSecret: process.env.LISTENER_CLIENT_SECRET,
onTokenRefresh: (next) => db.saveTokens(user.id, next), // always store the newest refresh token
});Other languages#
The OpenAPI 3.1 document at https://api.listener.com/v1/openapi.json works with standard generators, for example openapi-generator for Go, Java or Ruby, and imports into Postman, Insomnia or Bruno.