---
title: "Build a copyright scanner for user-uploaded content"
description: "Scan user uploads for copyrighted music with the AudD enterprise endpoint, capturing ISRC, UPC, label, and timestamps for every match."
slug: "/resources/recipes/ugc-copyright-scanner"
section: "recipes"
keywords: [audd, copyright detection, ugc moderation, isrc, upc, enterprise endpoint]
---

# Build a copyright scanner for user-uploaded content

If your platform accepts audio or video uploads, you often need to know
whether a file contains copyrighted music before you publish it. This recipe
builds a scanner: a user uploads a file, your backend sends it to AudD's
enterprise endpoint, and you get back every recognized track with its label,
ISRC, UPC, and the timestamps where it appears.

## What you'll build

A single backend endpoint — `POST /scan` — that accepts an uploaded file,
forwards it to AudD's enterprise endpoint, and returns a verdict: a list of
matched tracks (with identifiers and timestamps) or an empty list when the
file is clean. You decide the policy: block on any match, block above a
score threshold, or queue for human review.

The enterprise endpoint is the right choice here (not the standard
endpoint) for two reasons: user uploads are arbitrary length, and you want
*every* song in the file, not just one. The enterprise endpoint chunks the
file server-side and returns one match per recognized segment.

## Prerequisites

- An API token from [dashboard.audd.io](https://dashboard.audd.io). ISRC and
  UPC in responses require a Startup plan or higher.
- Python 3.10+ with the official SDK: `pip install audd`
- A few test files. `https://audd.tech/example.mp3` is a known track you can
  use to confirm the happy path.

## Walkthrough

### Step 1: Scan one file

Start with the smallest possible scan: send a file to the enterprise
endpoint and print what comes back. Always pass `limit` during development —
the enterprise endpoint bills per 12 seconds of audio processed, and an
unbounded call on a long file can ingest hours of audio.

```python
from audd import AudD

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

matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    return_metadata=["apple_music"],  # optional; adds streaming links
    limit=10,                          # cap matches while developing
)

for m in matches:
    print(f"{m.timecode}  {m.artist} — {m.title}  (ISRC {m.isrc})")
```

When you run this against the example file you'll see one match printed with
its timecode, artist, title, and ISRC. A clean file returns an empty list —
not an error.

> **Always set `limit` during development.** The enterprise endpoint bills
> per 12 seconds of audio processed. An unbounded call on a multi-hour upload
> can produce hundreds of metered matches. Start with `limit=10` and raise it
> only when you understand the cost on your real inputs.

### Step 2: Accept an upload and forward it

Now wire it to an HTTP handler. The SDK accepts a file path, raw bytes, or a
stream, so you can forward the uploaded bytes straight through without
writing them to disk.

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

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

@app.post("/scan")
async def scan(file: UploadFile):
    data = await file.read()
    matches = audd.recognize_enterprise(data, limit=25)
    return {
        "clean": len(matches) == 0,
        "matches": [
            {
                "timecode": m.timecode,
                "artist": m.artist,
                "title": m.title,
                "album": m.album,
                "label": m.label,
                "isrc": m.isrc,
                "upc": m.upc,
            }
            for m in matches
        ],
    }
```

`clean: true` with an empty `matches` list is your "no copyrighted music
detected" verdict.

### Step 3: Apply a policy

The raw match list is the input to whatever moderation policy you run. Three
common shapes:

```python
def decide(matches):
    if not matches:
        return "publish"                       # nothing recognized
    if any(m.label for m in matches):
        return "block"                         # a labelled (commercial) track
    return "review"                            # recognized but ambiguous
```

Block-on-any-match is the strictest policy. Most platforms instead block
only on commercial releases (a non-null `label`, or a present ISRC/UPC) and
send everything else to human review.

### Step 4: Keep cost bounded on long files

For long uploads where you only need a yes/no answer rather than a full
tracklist, sample the file instead of fingerprinting every chunk:

```python
matches = audd.recognize_enterprise(
    data,
    every=5,             # recognize every 5th chunk
    skip_first_seconds=0,
    limit=5,             # stop after 5 matches — enough for a verdict
)
```

`every=5` cuts the metered audio roughly five-fold while still catching
sustained use of a track. Use a full scan (`limit` high, no `every`) only
when you need the complete tracklist for evidence.

## What you get back

Each match in the list carries the core tags plus the identifiers that
matter for a copyright verdict:

```json
{
  "clean": false,
  "matches": [
    {
      "timecode": "00:31",
      "artist": "Imagine Dragons",
      "title": "Warriors",
      "album": "Smoke + Mirrors (Deluxe)",
      "label": "KIDinaKORNER/Interscope Records",
      "isrc": "USUM71414163",
      "upc": "00602547623805"
    }
  ]
}
```

| Field | Type | Meaning for a copyright verdict |
|---|---|---|
| `artist`, `title` | string \| null | What was recognized. Null on a custom-catalog match (see below). |
| `label` | string \| null | The releasing label. A non-null label is a strong commercial-release signal. |
| `isrc` | string \| null | International Standard Recording Code — the recording's unique ID. Cross-reference against licensing systems. |
| `upc` | string \| null | Universal Product Code — the release's ID. |
| `timecode` | string | Position within the *matched track* (not the upload) at the match point. |

`isrc` and `upc` are populated on enterprise responses for accounts on a
Startup plan or higher. If they come back null on every match, check your
plan tier.

This scanner answers "does the file contain copyrighted music, and which
tracks" — it does not place each match at a precise second in the file. If
you need exact in-file timestamps (for a takedown filing or a tracklist),
see [Generate DMCA takedown evidence](/resources/recipes/dmca-takedown-evidence),
which reads the per-chunk position the enterprise endpoint returns.

## Handling errors

The scanner only needs to distinguish a few cases:

- **Authentication errors** (`AudDAuthenticationError`) — bad or missing
  token. Fail loudly at startup, not per request.
- **Quota / subscription errors** (`AudDQuotaError`,
  `AudDSubscriptionError`) — you've hit a limit or the enterprise
  endpoint isn't enabled on your token. Surface to ops; don't retry.
- **Invalid-audio errors** (`AudDInvalidAudioError`) — the upload wasn't
  decodable audio/video. Treat as a user error: reject the upload with a
  "couldn't read this file" message rather than a server error.
- **Connection errors** (`AudDConnectionError`) — transient. Retry with
  backoff; the SDK already retries idempotent calls.

```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": "scan_failed"}, 502
```

## Going further

- Store matches keyed by upload ID so you can re-run policy without
  re-scanning (re-scanning re-bills).
- For *your own* catalog (detecting leaks of unreleased tracks, or sample
  reuse), upload your tracks to a custom catalog first — see
  [Detect samples in user uploads](/resources/recipes/sample-detection).
- To scan content that's already live behind a URL (a TikTok, a YouTube
  video) rather than an upload, pass the URL instead of bytes — AudD parses
  the social URL server-side.

---

**Related**

- [Standard, enterprise, or streams: how to choose](/resources/concepts/standard-vs-enterprise-vs-streams)
- [Enterprise cost optimization](/resources/concepts/enterprise-cost-control)
- [Generate DMCA takedown evidence](/resources/recipes/dmca-takedown-evidence)
- [Python SDK docs](https://docs.audd.io/sdks/python)
- [API reference](https://docs.audd.io)