---
title: "Generate DMCA takedown evidence"
description: "Produce a structured evidence pack — track identification plus exact in-file timestamps — proving copyrighted music appears in a piece of content, ready for a DMCA filing."
slug: "/resources/recipes/dmca-takedown-evidence"
section: "recipes"
keywords: [audd, dmca, takedown, copyright evidence, isrc, start_seconds]
---

# Generate DMCA takedown evidence

When you need to file a takedown over copyrighted music in someone else's
content, the complaint is only as strong as the evidence behind it: *which*
recording, *who* released it, and *exactly where* it plays. This recipe runs
a piece of offending content through AudD's enterprise endpoint, then assembles
a per-match evidence record — track identification plus precise start/end
timestamps in the file — and renders it both as JSON (for your records and
tooling) and as a human-readable summary (to paste into a complaint).

## What you'll build

A function that takes a URL or file for the offending content and returns an
evidence pack: one record per recognized track, each with the recording's
identification (artist, title, album, label, ISRC, UPC, song_link, score) and
its location in the content (file-absolute start and end timestamps), plus the
source reference. You'll render the pack two ways — structured JSON and a
plain-text summary.

The enterprise endpoint is the right tool: it handles arbitrary-length content
and finds *every* track rather than one. A complaint has to say when the
recording plays, and each match answers that with `start_seconds` and
`end_seconds` — the song's file-absolute position in the content.
`recognize_enterprise` requests accurate offsets by default, so those
positions are precise; there's no chunk-offset math to do.

> **AudD provides the identification data, not the legal filing.** This recipe
> produces evidence — what was recognized and where. Deciding whether to file,
> drafting the complaint, and asserting ownership are yours to handle. The
> output is input to that process, not legal advice.

## 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. ISRC and UPC
  in responses require a Startup plan or higher; they're returned on enterprise
  calls.
- Python 3.10+ with the SDK: `pip install audd`
- The offending content as a URL or a file. AudD parses social URLs
  server-side, so you can pass a link to live content directly.
  `https://audd.tech/example.mp3` works for a happy-path run.

## Walkthrough

### Step 1: Recognize the content

Run the content through `recognize_enterprise` — pass a URL, file bytes, or a
path. It handles any length and returns a flat `list[EnterpriseMatch]`: one
entry per recognized fragment, in time order, each carrying the precise
`start_seconds` / `end_seconds` the evidence is built on (accurate offsets are
requested by default).

```python
from audd import AudD

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


def recognize(content_url: str, limit: int = 25):
    """Return the enterprise matches — one per recognized fragment, in time order."""
    return audd.recognize_enterprise(
        content_url,
        limit=limit,                                 # cap metered fragments while developing
        return_metadata=["apple_music", "spotify"],  # optional streaming links per match
    )


matches = recognize("https://audd.tech/example.mp3", limit=25)

for m in matches:
    print(f"{m.start_seconds:.1f}s  {m.artist} — {m.title}  (score {m.score})")
```

Running this prints one line per recognized fragment. A recording that plays
for several minutes produces a run of consecutive matches all naming it —
Step 4 collapses that run into one evidence span. Clean content 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 long video can produce
> hundreds of metered fragments. Start with `limit=25` and raise it only when
> you understand the cost on your real inputs.

### Step 2: The timestamps the evidence cites

The timestamps a reviewer verifies against the content are `start_seconds` /
`end_seconds`: where this song plays in the content file, in seconds (e.g.
`246.78` to `251.64`). They're file-absolute and go into the evidence record
as-is. They're `None` only when a fragment arrived without a usable position —
skip those.

Two other fields are easy to mistake for them. `start_offset` / `end_offset`
are the raw offsets the seconds are derived from: milliseconds *within* AudD's
internal 12-second scan fragment (0–~12000), not file seconds; you rarely need
them. And `timecode` is the position inside the matched recording, not the
content file — it tells you which part of the song was playing, so never use
it to locate the infringement.

### Step 3: Format an in-file position into HH:MM:SS

A complaint wants a clock time, not raw seconds. Format the file-absolute
position:

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


print(fmt_hms(246.78))  # -> "00:04:06"
```

This always gives a zero-padded `HH:MM:SS`, which reads cleanly in a filing
even for content that runs past an hour.

### Step 4: Collapse a held track into one evidence span

For evidence you want *one* record with a time range — "this recording plays
from 00:04:06 to 00:06:18" — not ten near-identical records, one per fragment.
Collapse the consecutive matches for a track into a single span: the start is
the first match's `start_seconds`, the end is the last match's `end_seconds`.
A new `(artist, title)` opens a new span; a repeat of the one you're currently
extending stretches its end.

```python
def build_spans(matches) -> list[dict]:
    """Collapse consecutive same-track matches into one span with a time range."""
    spans = []
    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
        extending = (
            spans
            and spans[-1]["artist"] == m.artist
            and spans[-1]["title"] == m.title
        )
        if extending:
            spans[-1]["end"] = max(spans[-1]["end"], end)  # same track still playing
            continue
        spans.append({
            "artist": m.artist,
            "title": m.title,
            "album": m.album,
            "release_date": m.release_date,
            "label": m.label,
            "isrc": m.isrc,
            "upc": m.upc,
            "song_link": m.song_link,
            "score": m.score,
            "start": m.start_seconds,
            "end": end,
        })
    return spans
```

A two-minute play becomes one span with a real start and end, which is exactly
what a reviewer wants to verify against the content. The `start_seconds` guard
and the `end_seconds` fallback are there because the SDK parses responses
leniently: any field can be absent or `None`, and one positionless fragment
shouldn't sink the whole pack.

### Step 5: Keep only the spans worth citing

Not every recognition is takedown-grade evidence. A defensible record points at
a commercial release — one with a label and/or an ISRC/UPC. Filter to spans
that carry those identifiers, and drop the rest (or route them to manual
review).

```python
def is_citable(span: dict) -> bool:
    """A span worth putting in a takedown: identifies a commercial release."""
    return bool(span.get("label") or span.get("isrc") or span.get("upc"))


citable = [s for s in build_spans(matches) if is_citable(s)]
```

A non-null `label` or a present `isrc`/`upc` is your signal that the match is a
released recording you can name precisely. ISRC and UPC require a Startup plan
or higher; if they're null on every span, check your plan tier before assuming
the content is unidentifiable.

### Step 6: Assemble an evidence record per span

For each citable span, build a record with two halves: **identification** (what
the recording is and who released it) and **location** (where it plays in the
file). Add the source reference so the record stands on its own.

```python
from datetime import datetime, timezone


def evidence_record(span: dict, source: str) -> dict:
    return {
        "identification": {
            "artist": span["artist"],
            "title": span["title"],
            "album": span["album"],
            "release_date": span["release_date"],
            "label": span["label"],
            "isrc": span["isrc"],
            "upc": span["upc"],
            "song_link": span["song_link"],
            "score": span["score"],
        },
        "location": {
            "start_seconds": round(span["start"], 2),
            "end_seconds": round(span["end"], 2),
            "start_timestamp": fmt_hms(span["start"]),
            "end_timestamp": fmt_hms(span["end"]),
        },
        "source": source,
    }


def evidence_pack(matches, source: str) -> dict:
    spans = [s for s in build_spans(matches) if is_citable(s)]
    return {
        "source": source,
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "match_count": len(spans),
        "records": [evidence_record(s, source) for s in spans],
    }
```

`source` is the URL or filename of the offending content — record it explicitly
so each pack is self-describing. The `location` times are file-absolute: they
come straight from each match's `start_seconds` / `end_seconds`, so they point
at the exact moment in the content a reviewer can seek to.

### Step 7: Generate the pack end to end

```python
def build_pack(source: str) -> dict:
    matches = recognize(source, limit=50)
    return evidence_pack(matches, source)


pack = build_pack("https://audd.tech/example.mp3")
```

`pack` is the structured evidence — store it as JSON alongside your case
records.

### Step 8: Render a human-readable summary

The JSON is for your systems. For the complaint itself, render a plain-text
summary a human can read and a reviewer can verify against the content.

```python
def render_summary(pack: dict) -> str:
    lines = [
        f"Evidence pack for: {pack['source']}",
        f"Generated (UTC): {pack['generated_at']}",
        f"Recognized recordings: {pack['match_count']}",
        "",
    ]
    for i, rec in enumerate(pack["records"], 1):
        ident = rec["identification"]
        loc = rec["location"]
        lines += [
            f"[{i}] {ident['artist']} — {ident['title']}",
            f"     Album:  {ident['album']}",
            f"     Label:  {ident['label']}",
            f"     ISRC:   {ident['isrc']}",
            f"     UPC:    {ident['upc']}",
            f"     Link:   {ident['song_link']}",
            f"     Plays:  {loc['start_timestamp']}–{loc['end_timestamp']} "
            f"({loc['start_seconds']}s–{loc['end_seconds']}s into the content)",
            "",
        ]
    return "\n".join(lines)


print(render_summary(pack))
```

This produces something you can paste straight into a takedown form: each
recording named, identified, and located by its position in the file.

## What you get back

Every match in the flat list is one recognized fragment, in time order. The
fields the evidence uses:

| Field | Type | Role in the evidence |
|---|---|---|
| `start_seconds`, `end_seconds` | float \| None | Where this song plays **in the content file**, in seconds. The values the evidence is built on. `None` only when a fragment had no usable position. |
| `artist`, `title` | str \| None | The recording, named. |
| `album` | str \| None | The release it appears on. |
| `release_date` | str \| None | The release date of the recording. |
| `label` | str \| None | The releasing label — establishes the commercial rights holder. |
| `isrc` | str \| None | International Standard Recording Code — the recording's unique identifier. The strongest single identifier in a filing. |
| `upc` | str \| None | Universal Product Code — the release's identifier. |
| `song_link` | str \| None | `lis.tn` universal link to the track. |
| `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 recording**, not the content file. Supporting detail; don't use it to locate the infringement. |

For the example file, a match might come back with `start_seconds = 246.78`
(`00:04:06`) and `end_seconds = 251.64` (`00:04:11`) — the span the evidence
record cites.

`isrc` and `upc` populate on enterprise responses for Startup-plan accounts and
higher. A null ISRC/UPC doesn't mean the match is invalid — `label` plus
`artist`/`title` still identify the recording — but an ISRC makes the strongest
record.

## Handling errors

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

- **Authentication errors** (`AudDAuthenticationError`) — bad or missing token.
  Fail at startup.
- **Quota / subscription errors** (`AudDSubscriptionError`) — request limit hit,
  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 content URL or file wasn't decodable
  audio/video. For evidence work this matters: a record built on content the
  API couldn't actually read is worthless. Treat it as a hard failure for that
  source, not a silent empty pack.
- **Connection errors** (`AudDConnectionError`) — transient. Retry with backoff.

```python
from audd.errors import AudDError

try:
    pack = build_pack(source)
except AudDError as e:
    # any AudD API/transport failure — log and decide; retry connection errors
    print(f"Recognition failed: {e}")
    pack = None
```

An empty `records` list (no citable matches) is a valid, distinct outcome from
an error — it means the content recognized nothing commercial, so there's
nothing to file on.

## Going further

- Re-running recognition re-meters every fragment. Generate the pack once per
  source and persist the JSON; regenerate the human-readable summary from
  stored JSON rather than re-scanning.
- Reading metadata fields beyond the ones above — including undocumented or
  beta fields the API returns — comes through each match's `model_extra`. If a
  filing needs a field not listed here, read it off `m.model_extra` on the match.
- To find offending content in the first place — scanning uploads as they
  arrive — see
  [Build a copyright scanner for user-uploaded content](/resources/recipes/ugc-copyright-scanner).
- For multi-hour content, see
  [Enterprise cost optimization](/resources/concepts/enterprise-cost-control)
  on trading coverage against metered chunks.

---

**Related**

- [Build a copyright scanner for user-uploaded content](/resources/recipes/ugc-copyright-scanner)
- [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)