Skip to content
Listener.comDevelopers

Partner integrations

Connect your platform to Listener end to end, for example a podcast host. OAuth sign-in, show identity, social accounts, automatic clusters, webhooks, metrics, co-branded pages and ad-server delivery.

6 min read

This guide walks your platform through a complete Listener integration, for example a podcast host that wants its creators to see how every episode travels across YouTube, Instagram, TikTok and the rest. Each step uses a real endpoint; you can stop after any of them.

Partner integration sequence between the creator, your platform and ListenerCreatorYour platformListenerClicks “Connect Listener”1GET /oauth/authorize (PKCE)2Signs in or signs up, approves3Redirect with code4POST /oauth/token5PUT /v1/shows/{id}/podcast6POST + GET account-discovery7PUT integrations/{platform}8400 oauth_required + connect_url9Sends creator to connect_url10PUT auto-build · POST /v1/cluster-builds11POST /v1/webhook-endpoints12cluster.* · integration.* webhooks13GET episodes/lookup · GET cluster14PATCH cluster co_brand15POST campaigns/{id}/external-delivery16
Solid arrows are requests; dashed arrows are redirects, responses and webhooks.

Before you start#

  • An OAuth app. Create one under Settings → Developers → OAuth apps with your redirect URI. See OAuth 2.0.
  • Scopes. This guide uses shows:write episodes:read clusters:write campaigns:write webhooks:write offline_access. Each :write scope includes its :read twin. Add analytics:read if you also show account-level analytics. Ask only for what you use; see Scopes.
  • One connection per workspace. A token belongs to one Listener workspace: a creator’s show, or a network with many shows. Store tokens per workspace, keyed by workspace.id from the token response.

1. Add a “Connect Listener” button#

When the creator clicks it, send them to Listener’s authorization endpoint with PKCE. Use account=creator to open the Listener creator app directly; leave it out to let people choose between Listener and Listener Networks.

Node.js
import crypto from 'node:crypto';

export function connectListenerUrl(session) {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const state = crypto.randomBytes(16).toString('base64url');
  session.listener = { verifier, state };
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: process.env.LISTENER_CLIENT_ID,
    redirect_uri: 'https://your-platform.example/integrations/listener/callback',
    scope: 'shows:write episodes:read clusters:write campaigns:write webhooks:write offline_access',
    state,
    code_challenge: crypto.createHash('sha256').update(verifier).digest('base64url'),
    code_challenge_method: 'S256',
    account: 'creator',
  });
  return `https://api.listener.com/oauth/authorize?${params}`;
}

2. Sign in with Listener#

The creator signs in to Listener. Someone without an account can sign up on the same screen and comes straight back to the consent screen, which names your app, the workspace being shared and every scope. After they approve, Listener redirects to your callback with code and state. Exchange the code:

Node.js
const res = await fetch('https://api.listener.com/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    Authorization: 'Basic ' + Buffer.from(`${process.env.LISTENER_CLIENT_ID}:${process.env.LISTENER_CLIENT_SECRET}`).toString('base64'),
  },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: query.code,
    redirect_uri: 'https://your-platform.example/integrations/listener/callback',
    code_verifier: session.listener.verifier,
  }),
});
const tokens = await res.json();
// tokens.workspace = { id, name, kind } ; store access_token, refresh_token and expiry per workspace

Then find the show. A creator workspace has one; a network can have many, so let the person pick when there is more than one.

curl
curl https://api.listener.com/v1/shows \
  -H "Authorization: Bearer $LAT"

Every request below sends Authorization: Bearer lat_…. Refresh the access token before it expires after one hour (see Refreshing tokens).

3. Set the podcast identity#

Tell Listener which podcast the show is, so episodes and analytics line up with yours. Send the ids you know; only the fields you send change, and platform URLs are accepted too.

curl
curl -X PUT https://api.listener.com/v1/shows/$SHOW_ID/podcast \
  -H "Authorization: Bearer $LAT" \
  -H "Content-Type: application/json" \
  -d '{
    "rss_url": "https://feeds.your-platform.example/the-long-game.xml",
    "apple_podcasts_id": "https://podcasts.apple.com/us/podcast/the-long-game/id1580012345",
    "spotify_id": "https://open.spotify.com/show/5aBcDeFgHiJkLmNoPqRsTu"
  }'

These ids are references for matching. They don’t connect Spotify or Apple Podcasts analytics; those need the creator (step 4).

4. Find and connect social accounts#

Run account discovery to find the show’s public accounts on YouTube, Instagram, TikTok, X, Facebook, Threads and LinkedIn. Each run is metered and capped per show, so it needs confirm: true.

curl
curl -X POST https://api.listener.com/v1/shows/$SHOW_ID/account-discovery \
  -H "Authorization: Bearer $LAT" \
  -H "Content-Type: application/json" \
  -d '{"confirm": true}'

If the response says running, read the result a minute later with GET /v1/shows/{show_id}/account-discovery. Each candidate has a handle, profile URL, follower count, a confidence (high, medium, low), a reason, its current connection state and connect_via:

  • api: you can connect it yourself. Show the candidate to the creator, then confirm it with PUT /v1/shows/{show_id}/integrations/{platform}.
  • app_sign_in: a person has to sign in to the platform. Send the creator to verify_url.
Node.js
async function connectAccount(showId, platform, account) {
  const res = await fetch(`https://api.listener.com/v1/shows/${showId}/integrations/${platform}`, {
    method: 'PUT',
    headers: { Authorization: `Bearer ${lat}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(account), // { handle: 'thelonggame' } or { url: 'https://www.linkedin.com/company/…' }
  });
  const body = await res.json();
  if (res.status === 400 && body.error.code === 'oauth_required') {
    return { needsCreator: true, url: body.error.connect_url };
  }
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body;
}
PlatformHow it connects
instagram, tiktok, twitter, facebook, threadshandle or profile url
linkedinprofile url
simplecastapi_key, optionally podcast_id. The key is never returned.
youtube, instagramBusiness, spotify, applePodcastsSign-in only. The API answers 400 oauth_required with connect_url, the show’s Integrations page in Listener; send the creator there.

A successful connection seeds the account, runs its first refresh and emits integration.connected. Connecting a platform that is already connected to another account replaces it, and the response warns you. Disconnect with DELETE /v1/shows/{show_id}/integrations/{platform}?confirm=true.

5. Build clusters automatically#

An episode cluster groups an episode with every clip and post made from it. Turn on auto-build and Listener’s hourly run builds clusters for new episodes from the last 14 days:

curl
curl -X PUT https://api.listener.com/v1/shows/$SHOW_ID/auto-build \
  -H "Authorization: Bearer $LAT" \
  -H "Content-Type: application/json" \
  -d '{"mode": "suggest"}'

suggest records found posts for the creator to review; auto adds the posts Listener is sure about straight to the cluster. GET /v1/shows/{show_id}/auto-build shows the mode and recent per-episode statuses.

To build the moment you publish instead of waiting for the hourly run, start a build for that episode:

curl
curl https://api.listener.com/v1/cluster-builds \
  -H "Authorization: Bearer $LAT" \
  -H "Content-Type: application/json" \
  -d '{"show_id": "'$SHOW_ID'", "scope": "episodes", "episode_ids": ["'$EPISODE_ID'"], "mode": "suggest"}'

Builds run in the background. Follow one with GET /v1/cluster-builds/{job_id}, or wait for the webhook in the next step.

6. Subscribe to webhooks#

Create an endpoint in the creator’s workspace with their token. List event types explicitly; * also works but includes types you may not handle.

curl
curl https://api.listener.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $LAT" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-platform.example/webhooks/listener",
    "enabled_events": [
      "cluster.created", "cluster.updated", "cluster.nodes_added", "cluster_build.completed",
      "integration.connected", "integration.disconnected", "integration.stale"
    ],
    "description": "Your platform"
  }'

Store the returned secret per workspace and verify every delivery. Changes made in the Listener apps reach you within about five minutes; changes made through the API arrive within seconds. See Webhooks.

7. Map your episode ids#

Your platform has its own episode ids. Look up the Listener episode behind them with any identifier you have: the RSS item guid, apple_episode_id, spotify_episode_id, simplecast_episode_id, or title plus published_on.

curl
curl -G https://api.listener.com/v1/shows/$SHOW_ID/episodes/lookup \
  -H "Authorization: Bearer $LAT" \
  --data-urlencode "guid=your-platform-episode-8a1f3c2e" \
  --data-urlencode "published_on=2026-10-14"

best is set only for an unambiguous exact (an id or guid) or high (same title within a day) match; otherwise read matches and their confidence. Store the Listener episode_id and cluster_id next to your episode.

8. Read cluster metrics#

curl
curl https://api.listener.com/v1/shows/$SHOW_ID/clusters/$CLUSTER_ID \
  -H "Authorization: Bearer $LAT"

The cluster carries reach by category (YouTube, audio, social) and by format, engagement, every post with its platform numbers, and its public URL. Show freshness next to the numbers: when the cluster was last refreshed and any posts that failed to refresh. Cluster posts refresh when their public page is viewed and when you ask; POST /v1/shows/{show_id}/clusters/{cluster_id}/refresh with confirm: true refreshes every post now (it needs the refresh:write scope and spends data-provider credits). See Data sources, freshness & retention and Metrics glossary.

9. Co-brand the share page#

Every cluster has a public page the creator can share. Add your brand next to the show’s:

curl
curl -X PATCH https://api.listener.com/v1/shows/$SHOW_ID/clusters/$CLUSTER_ID \
  -H "Authorization: Bearer $LAT" \
  -H "Content-Type: application/json" \
  -d '{"customization": {"co_brand": {"name": "Your Platform", "logo_url": "https://your-platform.example/logo.png", "href": "https://your-platform.example"}}}'

name is required (1 to 80 characters); logo_url and href must be https://. Send "co_brand": null to remove it. Other customization fields are kept.

10. Push ad-server delivery onto campaigns#

If your platform serves ads, record delivered impressions (and optionally listeners) on the creator’s Listener campaigns, per day or per period, per slot or show:

curl
curl https://api.listener.com/v1/campaigns/$CAMPAIGN_ID/external-delivery \
  -H "Authorization: Bearer $LAT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: delivery-$CAMPAIGN_ID-2026-10-14" \
  -d '{
    "rows": [
      { "source": "other", "label": "The Long Game mid-roll", "slot_id": "'$SLOT_ID'", "date": "2026-10-14", "impressions": 18250, "listeners": 11900 }
    ]
  }'
  • source is one of the values the API accepts: adswizz, audioserve or other.
  • Send either date or start_date and end_date.
  • Re-sending the same source, slot, show and day (or period) updates that row instead of adding a duplicate, so a daily sync can safely resend.
  • Up to 500 rows per request and 2,000 per campaign.
  • Read them back with GET /v1/campaigns/{campaign_id}/external-delivery, which includes totals per source.

Ad-server counts stay separate: they are shown next to Listener’s numbers and never added to Listener reach, impressions or EMV.

What data is aggregate-only#

Listener works with aggregate counts: per platform, per day, per post or per episode. The API never returns listener-level data: no individual plays or listen events, IP addresses, device ids or user agents. External delivery takes aggregates too (impressions and listeners per day or period); don’t send per-listener records.

Limits and versioning#

  • Rate limits apply per OAuth connection, so every creator who connects gets their own budget: 120 requests a minute, 30 writes a minute, 10 in flight; analytics calls count double. See Rate limits.
  • Metered calls: account discovery and platform or cluster refreshes spend credits, need confirm: true and may be capped. Don’t run them on a timer for every show.
  • Retries: send an Idempotency-Key on writes so a retried request never applies twice. See Idempotency.
  • Version: send Listener-Version: 2026-10-14 on every request so new versions never change behaviour under you. See Versioning.
  • Test safely: every write accepts dry_run: true. See Testing with dry_run.
  • Disconnects: when a creator removes your app under Authorized apps, calls start failing with 401 credential_revoked. Treat that as a disconnect and stop syncing that workspace.