---
title: "Generate music credits for a podcast episode"
description: "Produce a timestamped music credits list for a podcast episode with the AudD enterprise endpoint, collapsing repeated matches into one credit per song."
slug: "/resources/recipes/podcast-music-credits"
section: "recipes"
keywords: [audd, podcast music credits, show notes, music licensing, enterprise endpoint]
---

# Generate music credits for a podcast episode

Podcasts use music: intros, stingers, bumpers, beds under the host, a song
played in full during a segment. This recipe takes one episode audio file and
produces a credits list — every song that appears, with a timestamp, artist,
title, and label — ready to drop into your show notes or your licensing
records.

## What you'll build

A script that sends a podcast episode to AudD's enterprise endpoint, walks the
matches it returns, collapses runs of the same song into a single credit with a
time range, and prints a clean credits block.

The enterprise endpoint is the right tool here (not the standard endpoint): an
episode is arbitrary length, and you want *every* song in it, not one.
`recognize_enterprise` returns a flat `list[EnterpriseMatch]` — one entry per
recognized fragment, in time order — and each match carries its position in
your episode directly as `start_seconds` / `end_seconds`. A two-minute outro
song spans several consecutive matches, so you'll get several near-identical
entries for it; the dedupe step turns those back into one line that reads
"used from 48:00 to 50:00."

The show-notes timestamps come straight from those matches: `start_seconds`
and `end_seconds` are the song's position in your episode, in seconds.
`recognize_enterprise` requests accurate offsets by default, so there's no
offset math to do — you read those values and group on them.

## Prerequisites

- An API token from [dashboard.audd.io](https://dashboard.audd.io). Get your
  own token there; the enterprise endpoint must be enabled on it. `label` comes
  back on enterprise responses; ISRC and UPC require a Startup plan or higher if
  you also want to log those for licensing.
- Python 3.10+ with the SDK: `pip install audd`
- A podcast episode file or URL. `https://audd.tech/example.mp3` is a public,
  reproducible file you can run against to see the full flow before you point
  it at your own audio.

## Walkthrough

### Step 1: Recognize the episode

Point `recognize_enterprise` at the episode — a URL, file bytes, or a path. It
scans the whole file and returns a flat `list[EnterpriseMatch]`, one entry per
recognized fragment in time order, each with a file-absolute `start_seconds` /
`end_seconds` (accurate offsets are requested by default).

```python
from audd import AudD

audd = AudD("your-api-token")  # token from dashboard.audd.io

# limit caps metered fragments while you get the credits logic right
matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    limit=20,
    return_metadata=["apple_music", "spotify"],  # optional streaming links per match
)

for m in matches:
    if m.start_seconds is None:
        continue
    print(f"{m.start_seconds:.0f}s  {m.artist} — {m.title}")
```

The output is one line per recognized fragment. Stretches of pure speech (the
host talking with no music bed) produce no match — that's a gap, not an error.
A song that plays for two minutes shows up as a run of consecutive matches
with the same artist and title but advancing `start_seconds` values; that
repetition is exactly what Step 3 folds away.

> **Always set `limit` during development.** The enterprise endpoint bills per
> 12 seconds of audio processed. A 90-minute episode is hundreds of fragments;
> an unbounded call meters all of them. Develop against `limit=20`, confirm your
> credits logic, and only then raise the cap for a full pass.

### Step 2: Understand the time fields

Before deduping, get the time fields straight — they are easy to confuse and
the credits block depends on using the right one.

- **`start_seconds` / `end_seconds`** — where this song plays in your episode,
  in seconds (e.g. `64.2` to `71.6`). These are the values you put in the show
  notes; a fragment that arrived without a usable position has `None` here.
- `start_offset` / `end_offset` — raw milliseconds *within* AudD's internal
  12-second scan fragment, which the seconds are derived from. Not episode
  seconds, and rarely needed.
- `timecode` — the position inside the matched song where the fragment lined
  up (e.g. `00:41` means the fragment matched 41 seconds into that song).
  Useful for knowing *which part* of a song was used, but it is not a position
  in your episode.

Everything below keys off `start_seconds` and `end_seconds`.

### Step 3: Collapse consecutive matches into one credit

The dedupe rule: walk the matches in time order; whenever the current match has
the same artist and title as the credit you're currently building, extend that
credit's end time instead of starting a new one. A new artist/title starts a new
credit.

```python
def build_credits(matches) -> list[dict]:
    """Collapse runs of the same song into one credit with a time range."""
    credits = []
    for m in matches:
        if m.start_seconds is None:
            continue  # no usable position for this match — skip it
        end = m.end_seconds if m.end_seconds is not None else m.start_seconds + 12
        same_as_last = (
            credits
            and credits[-1]["artist"] == m.artist
            and credits[-1]["title"] == m.title
        )
        if same_as_last:
            # Same song still playing — extend the running credit.
            credits[-1]["end"] = max(credits[-1]["end"], end)
        else:
            credits.append({
                "artist": m.artist,
                "title": m.title,
                "label": m.label,
                "start": m.start_seconds,
                "end": end,
                "song_link": m.song_link,
            })
    return credits
```

The result is one entry per distinct song, in the order it first appears, with
the span it occupied. The SDK parses responses leniently — any field can be
absent or `None` — hence the `start_seconds` guard and the `end_seconds`
fallback, which keep a positionless fragment from crashing the pass.

> **A song that recurs gets two credits, on purpose.** If the same theme plays
> at the open and again at the close, two non-adjacent runs produce two
> credits. That's usually what you want in show notes — "used at 00:00 and
> again at 48:00." If you'd rather merge all appearances of a song into one
> line, group the finished credits by `(artist, title)` afterward.

### Step 4: Render the credits block

Format each credit as a timestamp range plus artist, title, and label.

```python
def fmt_time(seconds: float) -> str:
    s = int(seconds)
    h, rem = divmod(s, 3600)
    m, sec = divmod(rem, 60)
    return f"{h:02d}:{m:02d}:{sec:02d}" if h else f"{m:02d}:{sec:02d}"


def render(credits: list[dict]) -> str:
    lines = ["Music credits", ""]
    for c in credits:
        span = f"{fmt_time(c['start'])}–{fmt_time(c['end'])}"
        label = f" ({c['label']})" if c["label"] else ""
        lines.append(f"{span}  {c['artist']} — {c['title']}{label}")
    return "\n".join(lines)


matches = audd.recognize_enterprise("https://audd.tech/example.mp3", limit=20)
print(render(build_credits(matches)))
```

This prints a block like:

```text
Music credits

00:00–00:57  Tears For Fears — Everybody Wants To Rule The World (UMC (Universal Music Catalogue))
```

Songs that played only behind a few seconds of speech collapse to a short
span; a full segment track collapses to its real run. Either way you get one
line per song.

### Step 5: Skip the cold open, sample long beds

Two parameters keep the cost and the noise down on real episodes. Pass them as
keyword arguments to `recognize_enterprise`:

```python
matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    skip_first_seconds=30,  # ignore a fixed ad slot or cold open
    every=1,
    skip=0,
    limit=50,
)
```

- `skip_first_seconds` starts recognition partway in — handy if every episode
  opens with the same dynamically-inserted ad you don't want to credit.
- `every` and `skip` sample the file: `every=1, skip=4` recognizes one fragment
  then skips the next four (one match per minute). For credits you usually want
  full coverage so you don't miss a short stinger, but for a rough "what music
  is in here" pass, sampling cuts metered fragments several-fold.

Note `skip_first_seconds` must not be combined with `use_timecode`; use one or
the other.

## What you get back

Each entry in the returned list is one recognized fragment, in time order.
The fields a credits list cares about:

| Field | Type | Meaning for a credits list |
|---|---|---|
| `start_seconds`, `end_seconds` | float \| None | Where this song plays **in your episode**, in seconds. These are the show-notes timestamps you group and render on. `None` only when a fragment had no usable position. |
| `artist`, `title` | str \| None | The credit. |
| `album`, `release_date` | str \| None | The release the song appears on, and when. |
| `label` | str \| None | The releasing label — what you cite for licensing. |
| `isrc`, `upc` | str \| None | Recording / release identifiers for licensing records (Startup plan or higher). |
| `song_link` | str \| None | A `lis.tn` universal link to the track; handy as a clickable credit. |
| `score` | int \| None | Match confidence for the fragment. |
| `start_offset`, `end_offset` | int \| None | Raw milliseconds **within** the 12-second scan fragment that `start_seconds`/`end_seconds` are derived from. Rarely needed directly. |
| `timecode` | str \| None | Position **inside the matched song**, not your episode. Don't put this in show notes. |

For the example file, a match might come back with `start_seconds = 0.0`,
`end_seconds = 57.0`, `artist = "Tears For Fears"`, `title = "Everybody Wants To
Rule The World"`, and `label = "UMC (Universal Music Catalogue)"` — which
renders as the single credit line above.

## Handling errors

The SDK raises typed exceptions; catch the ones you can act on.

- **Authentication errors** (`AudDAuthenticationError`) — bad or missing token.
  Fail at startup, not per episode.
- **Quota / subscription errors** (`AudDSubscriptionError`) — you've hit a
  request limit, or the enterprise endpoint isn't enabled on your token. The
  enterprise endpoint and ISRC/UPC both depend on plan tier; surface these to
  the account owner rather than retrying.
- **Invalid-audio errors** — the URL or file wasn't decodable audio. Treat as a
  bad-input case: report which episode failed and move on.
- **Connection errors** (`AudDConnectionError`) — transient; retry with backoff.

```python
from audd.errors import AudDError

try:
    matches = audd.recognize_enterprise(episode_url, limit=50)
    credits = build_credits(matches)
except AudDError as e:
    # any AudD API/transport failure — log and decide; retry connection errors
    print(f"Recognition failed: {e}")
    credits = []
```

A clean stretch with no music isn't an error — those fragments simply produce no
match, so they contribute no credits.

## Going further

- Persist the finished credits keyed by episode ID so you never re-scan (a
  re-scan re-meters every fragment).
- If your show always opens and closes with the same theme, post-process the
  credits to merge non-adjacent runs of the same `(artist, title)` into one
  line.
- Recognizing a back catalog of episodes? See
  [Enterprise cost optimization](/resources/concepts/enterprise-cost-control)
  for how `every`, `skip`, and `limit` trade coverage against metered fragments.
- Turning a DJ set or continuous mix into a tracklist instead of a credits
  block? See [Turn a DJ set or mix into a tracklist](/resources/recipes/dj-set-tracklist).

---

**Related**

- [Turn a DJ set or mix into a tracklist](/resources/recipes/dj-set-tracklist)
- [Enterprise cost optimization](/resources/concepts/enterprise-cost-control)
- [Standard, enterprise, or streams: how to choose](/resources/concepts/standard-vs-enterprise-vs-streams)
- [Python SDK docs](https://docs.audd.io/sdks/python)
- [API reference](https://docs.audd.io)