Recipe

Build a copyright scanner for user-uploaded content

Scan user uploads for copyrighted music with the AudD enterprise endpoint, capturing ISRC, UPC, label, and timestamps for every match.

view .md auddcopyright detectionugc moderationisrc

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. 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.

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.

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:

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:

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:

{
  "clean": false,
  "matches": [
    {
      "timecode": "00:31",
      "artist": "Imagine Dragons",
      "title": "Warriors",
      "album": "Smoke + Mirrors (Deluxe)",
      "label": "KIDinaKORNER/Interscope Records",
      "isrc": "USUM71414163",
      "upc": "00602547623805"
    }
  ]
}
FieldTypeMeaning for a copyright verdict
artist, titlestring | nullWhat was recognized. Null on a custom-catalog match (see below).
labelstring | nullThe releasing label. A non-null label is a strong commercial-release signal.
isrcstring | nullInternational Standard Recording Code — the recording’s unique ID. Cross-reference against licensing systems.
upcstring | nullUniversal Product Code — the release’s ID.
timecodestringPosition 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, 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.
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.
  • 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

Reading this as an AI agent? The raw Markdown is at recipes/ugc-copyright-scanner.md, and the full index is /resources/llms.txt.