---
title: "Detect samples and reuse of your own tracks"
description: "Detect when uploaded content reuses your own tracks — samples, leaks, or re-uploads — by matching against a private custom catalog with the AudD API."
slug: "/resources/recipes/sample-detection"
section: "recipes"
keywords: [audd, sample detection, custom catalog, leak detection, audio_id, fingerprint]
---

# Detect samples and reuse of your own tracks

If you own a catalog — a label, a sample pack, unreleased masters, a
production library — you often need the opposite of commercial-music
detection. The question is "is *my* track in here", not "is there *any*
copyrighted music here". This recipe uploads your own recordings to a private custom
catalog on your account, then runs incoming content through recognition and
identifies the matches that came from your catalog rather than the public
database.

## What you'll build

A two-part workflow. First, a one-time (or ongoing) ingestion step: your
tracks go into your account's private fingerprint database via
`POST api.audd.io/upload/`. Second, a recognition path that sends incoming
content through the API and inspects each match to answer one question —
*did this match come from my private catalog, or from the public 160-million-song
database?*

The distinguishing signal is the `audio_id` field. A custom-catalog match
carries an `audio_id` (the ID of the track you uploaded), and its `artist`
and `title` may be null, because a private master isn't a public release
with public tags. A public-database match has `artist`/`title` populated and
no `audio_id`. That single check is what separates "someone reused my track"
from "someone used a commercial song" — and the latter is the
[copyright-scanner recipe](/resources/recipes/ugc-copyright-scanner), not
this one.

## Prerequisites

- An API token from [dashboard.audd.io](https://dashboard.audd.io).
- Custom-catalog upload access. The upload endpoint requires special access
  on your account — email [api@audd.io](mailto:api@audd.io) to have it
  enabled. Recognition against your catalog works on your normal token once
  the tracks are ingested.
- Python 3.10+ with the official SDK: `pip install audd`
- The tracks you want to detect, as audio files (MP3, WAV, FLAC, M4A, OGG,
  AAC, WMA, or AIFF).

## Walkthrough

### Step 1: Ingest your tracks into the custom catalog

Uploading a track fingerprints it and stores it in your account's private
database. From then on, any recognition call on your token can match against
it. This is a fingerprint store, not file storage — you're teaching the API
what your recordings sound like, not hosting the files.

Upload requires special access on your account (email
[api@audd.io](mailto:api@audd.io) to enable it), but the call itself is
simple: you send each track with an integer **`audio_id` that you
assign** — your own track ID — and AudD fingerprints it under that ID. The ID
is yours to choose; it's what comes back on every later recognition that
matches this track. The upload response itself carries no payload
(`{"status": "success", "result": null}`), so there's nothing to read back from
it — you already hold the ID, because you set it.

`custom_catalog.add(audio_id, source)` takes the ID you assign and the track (a
path, URL, or bytes):

```python
from audd import AudD

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

# You assign each track an integer audio_id — your own track ID. It's what
# comes back on a later recognition that matches this track.
catalog = {
    51885: {"path": "masters/unreleased-01.wav", "title": "Unreleased 01"},
    51886: {"path": "masters/unreleased-02.wav", "title": "Unreleased 02"},
}

for audio_id, meta in catalog.items():
    audd.custom_catalog.add(audio_id, meta["path"])  # fingerprint under your id
```

> **The custom catalog is private to your account.** Tracks you upload are
> only matchable from your own token. They are not added to the public
> 160-million-song database, and other accounts cannot recognize against them.

Maintain your own mapping from `audio_id` to whatever your track means to you
(release name, internal ID, rights holder). The API stores the fingerprint
and the `audio_id`; the business meaning lives on your side.

### Step 2: Recognize incoming content

Now run a piece of incoming content through recognition. A single
recognition call checks the audio against *both* the public database and
your private catalog at once — you don't choose one or the other. What
differs is how you read the result.

For short clips, use the standard endpoint. For full tracks, DJ sets,
videos, or anything of arbitrary length, use the enterprise endpoint (it
chunks server-side and returns one match per recognized segment). Always set
`limit` on enterprise calls during development.

```python
from audd import AudD

audd = AudD("your-api-token")

# Short clip (under ~25 s of audio): standard endpoint, one match or None.
match = audd.recognize("https://audd.tech/example.mp3")

# Arbitrary-length content: enterprise endpoint, list of matches.
matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    limit=10,  # cap matches while developing; enterprise bills per 12 s
)
```

The standard call returns a single match or `None` on no match. The
enterprise call returns a list (empty on no match). Neither raises on
"nothing recognized" — an empty/None result is a clean verdict, distinct
from an error.

### Step 3: Classify each match — yours or public

This is the core of the recipe. For every match, check `audio_id`. If it's
set, the match came from your private catalog; look it up in your local map.
If it's absent, it's a public-database match (commercial music), which this
recipe ignores.

```python
def classify(match, catalog):
    """Return ('mine', meta) for a custom-catalog hit, ('public', tags) otherwise."""
    audio_id = getattr(match, "audio_id", None)
    if audio_id:
        # Custom-catalog match. artist/title are often null on private tracks,
        # so rely on audio_id + your own mapping for identification.
        meta = catalog.get(audio_id, {"title": "(unknown — not in local map)"})
        return "mine", {
            "audio_id": audio_id,
            "my_track": meta["title"],
            "timecode": match.timecode,
            "artist": match.artist,   # may be null
            "title": match.title,     # may be null
        }
    # No audio_id => public-database match (commercial release).
    return "public", {
        "artist": match.artist,
        "title": match.title,
        "label": match.label,
    }


def find_my_tracks(matches, catalog):
    return [info for kind, info in (classify(m, catalog) for m in matches)
            if kind == "mine"]
```

`audio_id` is the only reliable discriminator. Don't key off `artist`/`title`
being null — a public match can have sparse tags too. The presence of
`audio_id` is what means "this is from your catalog".

### Step 4: Wire it into an upload handler

Putting it together: accept incoming content, recognize it, and report which
of your tracks (if any) it reuses.

```python
from fastapi import FastAPI, UploadFile
from audd import AudD

app = FastAPI()
audd = AudD("your-api-token")
# catalog: your audio_id -> metadata map from Step 1, loaded at startup.

@app.post("/check-reuse")
async def check_reuse(file: UploadFile):
    data = await file.read()
    matches = audd.recognize_enterprise(data, limit=25)
    mine = find_my_tracks(matches, catalog)
    return {
        "reuses_my_catalog": len(mine) > 0,
        "my_matches": mine,
    }
```

`reuses_my_catalog: true` with a populated `my_matches` list is your
"this content contains one of my tracks" verdict — a sample, a leak of an
unreleased master, or a straight re-upload, depending on what you put in the
catalog and where the content came from.

## What you get back

A custom-catalog match and a public match differ in exactly the fields you
key on. Here are both, side by side, as they appear in the result:

```json
{
  "custom_catalog_match": {
    "audio_id": "742918",
    "artist": null,
    "title": null,
    "album": null,
    "label": null,
    "timecode": "00:14",
    "song_link": null
  },
  "public_match": {
    "audio_id": null,
    "artist": "Imagine Dragons",
    "title": "Warriors",
    "album": "Smoke + Mirrors (Deluxe)",
    "label": "KIDinaKORNER/Interscope Records",
    "timecode": "00:31",
    "song_link": "https://lis.tn/Warriors"
  }
}
```

| Field | Type | Meaning for sample/reuse detection |
|---|---|---|
| `audio_id` | string \| null | **The discriminator.** Set only on custom-catalog matches; it's the ID of the track you uploaded. Null means the match came from the public database. |
| `artist`, `title` | string \| null | May be null on a custom-catalog match — a private master has no public tags. Don't rely on these to identify your track; use `audio_id` against your own map. |
| `album`, `label` | string \| null | Typically null for private uploads, populated for public commercial releases. |
| `timecode` | string | Position within the *matched track* at the match point — i.e. how far into *your* track the reused segment starts. |
| `song_link` | string \| null | A `lis.tn` universal link for public releases; null for private catalog tracks (they aren't public). |

On enterprise responses each match additionally carries `start_offset` and
`end_offset` — the seconds into the *incoming* content where the match
begins and ends. Use those to point at *where in the upload* your track was
reused; use `timecode` to know *which part of your track* was lifted.

## Handling errors

This workflow has two distinct failure surfaces — ingestion and recognition.

- **Authentication errors** (`AudDAuthenticationError`) — bad or missing
  token. Fail at startup.
- **Subscription / access errors** (`AudDSubscriptionError`) — the
  upload endpoint isn't enabled on your token. Custom-catalog upload needs
  special access; email [api@audd.io](mailto:api@audd.io). This surfaces on
  ingestion, not on recognition.
- **Invalid-audio errors** (`AudDInvalidAudioError`) — the file (whether
  a track you're ingesting or content you're scanning) wasn't decodable
  audio/video. Treat as a user/input error, not a server error.
- **Connection errors** (`AudDConnectionError`) — transient. Retry with
  backoff. Do **not** blindly auto-retry an upload/ingest call — a retried
  ingest can fingerprint the same track twice and hand you two `audio_id`s.
  Make ingestion idempotent on your side (track which files you've already
  uploaded).

```python
from audd.errors import AudDInvalidAudioError, AudDAPIError

try:
    matches = audd.recognize_enterprise(data, limit=25)
except AudDInvalidAudioError:
    return {"error": "unreadable_file"}, 422
except AudDAPIError as e:
    # log e.error_code, e.request_id for support
    return {"error": "recognition_failed"}, 502
```

## Going further

- Reading additional fields beyond the typed result properties: every result
  and per-provider block round-trips unknown fields through Pydantic's
  `model_extra` map. If a custom-catalog response carries metadata you want that
  isn't a typed property, read it from `model_extra`.
- To detect *commercial* music in user uploads (the inverse of this recipe —
  someone using a song you don't own), see
  [Build a copyright scanner for user-uploaded content](/resources/recipes/ugc-copyright-scanner).
- To turn a confirmed match into a filing for a takedown, see
  [Generate DMCA takedown evidence](/resources/recipes/dmca-takedown-evidence).
- Monitoring live streams for reuse of your catalog (a Twitch or radio stream
  playing your tracks) uses the same `audio_id` classification on stream
  callbacks — see the streams endpoints.

---

**Related**

- [Build a copyright scanner for user-uploaded content](/resources/recipes/ugc-copyright-scanner)
- [Generate DMCA takedown evidence](/resources/recipes/dmca-takedown-evidence)
- [Python SDK docs](https://docs.audd.io/sdks/python)
- [API reference](https://docs.audd.io)