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.
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.mp3is 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
limitduring 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 withlimit=10and 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"
}
]
}
| 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, 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.
