ISRC Collision Resolution Strategies

An ISRC collision occurs when a single twelve-character International Standard Recording Code resolves to more than one physical or acoustic master, or, conversely, when one master recording carries more than one valid-looking code across a label’s delivery history. This is a distinct failure from a mismatched value reported by two storefronts for the same recording: a true collision means the identifier itself has been reused, mistyped into a registrant’s own allocation block, or independently assigned by two different rights holders who never checked the national registry. Getting the resolution order wrong is expensive, because every downstream calculation in the ISRC-to-ISWC Mapping Workflows chain inherits whichever recording the engine picked. What follows is a deterministic precedence model, a quarantine workflow, and the Python building blocks for enforcing both in a production reconciliation job.

ISRC Collision Resolution Trade-offs

Collisions can be resolved with several strategies, ranging from strict registry lookups to fully automated consensus voting. Each carries a different balance of accuracy, latency, and manual burden.

Resolution Strategy Precedence Logic False-Positive Risk Operational Cost Royalty Impact
National agency-of-record lookup Query the issuing agency’s registrant prefix (first 5 characters) to confirm which label legitimately owns the code Low — prefix ownership is authoritative Medium (requires agency API or bulk file access) Highest accuracy; misattributed payees are essentially eliminated when the prefix owner is confirmed
Earliest valid-registration timestamp The recording with the oldest verifiable ingestion or catalog-registration date wins the code Medium — clerical backdating can spoof this Low (pure metadata comparison) Protects legacy catalog holders but can silently favor stale metadata over a corrected re-delivery
Label-of-record self-attestation The label controlling the original master’s rights contract is asked to confirm ownership directly Low, but slow High (manual outreach, SLA delays) Very accurate but delays payout release; use only for high-value catalogs or repeated collisions
DSP consensus voting Treat the code as resolved to whichever recording a majority of reporting DSPs independently associate with it High — DSPs often mirror each other’s ingestion errors Low (aggregation query) Dangerous as a sole authority; DSP-reported values are advisory only and must never outrank the agency-of-record
Manual quarantine, no auto-resolution Freeze both candidate recordings from royalty calculation until a human reviewer closes the case None (by design) Highest (reviewer time, SLA backlog) Zero misallocation risk, but unresolved queue growth directly delays payee payouts if review capacity is under-resourced

The practical rule of thumb: agency-of-record lookup is the only strategy trusted to auto-resolve without review; every other signal is either a tie-breaker among agency-confirmed candidates or a trigger for quarantine.

Prerequisites & Assumptions

This implementation assumes Python 3.11+, pydantic>=2.4 for record validation, and a reconciliation store (Polars or a Postgres staging table) that already carries one row per delivered recording with columns for isrc, registrant_code, recording_fingerprint, label_of_record_id, and ingested_at. It assumes your pipeline has already run acoustic or metadata matching upstream — collision resolution operates on the output of that matching step, not on raw DSP payloads. It also assumes access to a cached extract of the national ISRC agency’s registrant-prefix table, refreshed at least weekly, since live agency API calls are too slow to run per-record in a batch job.

Implementation

1. Detect Collision Candidates

A collision candidate is any isrc value associated with more than one distinct recording_fingerprint, or any recording_fingerprint associated with more than one isrc. Both directions must be checked; they represent different real-world failures (reuse vs. over-issuance) but require the same resolution engine downstream.

python
import polars as pl
from pydantic import BaseModel, Field


class CollisionCandidate(BaseModel):
    isrc: str = Field(pattern=r"^[A-Z]{2}[A-Z0-9]{3}\d{2}\d{5}$")
    recording_fingerprints: list[str]
    label_of_record_ids: list[str]
    ingested_at_values: list[str]


def find_isrc_reuse(staging: pl.DataFrame) -> list[CollisionCandidate]:
    grouped = (
        staging.group_by("isrc")
        .agg(
            pl.col("recording_fingerprint").unique().alias("recording_fingerprints"),
            pl.col("label_of_record_id").unique().alias("label_of_record_ids"),
            pl.col("ingested_at").sort().alias("ingested_at_values"),
        )
        .filter(pl.col("recording_fingerprints").list.len() > 1)
    )
    return [CollisionCandidate.model_validate(row) for row in grouped.to_dicts()]


def find_isrc_overissuance(staging: pl.DataFrame) -> pl.DataFrame:
    return (
        staging.group_by("recording_fingerprint")
        .agg(pl.col("isrc").unique().alias("isrc_values"))
        .filter(pl.col("isrc_values").list.len() > 1)
    )

2. Apply Authority Precedence

Resolve each candidate by walking the precedence order from the trade-off table: agency-of-record confirmation first, then earliest valid registration, then quarantine.

python
from datetime import datetime


def load_registrant_prefixes() -> dict[str, str]:
    # Refreshed weekly from the national ISRC agency bulk export.
    return {"US-S1Z": "label_of_record_042", "GB-AAA": "label_of_record_017"}


def resolve_collision(
    candidate: CollisionCandidate, registrant_prefixes: dict[str, str]
) -> tuple[str | None, str]:
    prefix = candidate.isrc[:5]
    confirmed_owner = registrant_prefixes.get(prefix)

    if confirmed_owner and confirmed_owner in candidate.label_of_record_ids:
        matching_index = candidate.label_of_record_ids.index(confirmed_owner)
        return candidate.recording_fingerprints[matching_index], "agency_of_record"

    timestamps = [datetime.fromisoformat(t) for t in candidate.ingested_at_values]
    if len(set(timestamps)) == len(timestamps):
        earliest_index = timestamps.index(min(timestamps))
        return candidate.recording_fingerprints[earliest_index], "earliest_registration"

    return None, "quarantine"

3. Quarantine and Route Unresolved Collisions

Anything the precedence engine cannot close automatically must be frozen out of royalty calculation and written to a reviewable, append-only queue rather than silently defaulted.

python
import hashlib
from datetime import datetime, timezone


def quarantine_collision(candidate: CollisionCandidate, reason: str) -> dict:
    batch_id = hashlib.sha256(
        f"{candidate.isrc}:{sorted(candidate.recording_fingerprints)}".encode()
    ).hexdigest()[:16]
    return {
        "batch_id": batch_id,
        "isrc": candidate.isrc,
        "reason": reason,
        "candidates": candidate.recording_fingerprints,
        "quarantined_at": datetime.now(timezone.utc).isoformat(),
        "status": "PENDING_REVIEW",
    }

Verification & Validation

Before releasing a payout run, assert three invariants against the resolved set: every isrc in the royalty ledger maps to exactly one recording_fingerprint; every quarantined batch_id is absent from the payout join (a straight EXCEPT query against the ledger catches leaks); and the count of records resolved via agency_of_record divided by total candidates — your ISRC resolution rate — should exceed 90% in a mature catalog. A rate materially lower than that usually means your registrant-prefix cache is stale rather than a genuine spike in collisions. Log every resolution decision, including the precedence tier that closed it, to the same append-only trail used for Royalty Audit Trail Reference purposes, so a payee dispute can be traced back to the exact rule that assigned their recording.

Edge Cases & Gotchas

Watch for ISRCs that were legitimately re-registered after a catalog sale — the label_of_record_id changes but the recording is unchanged, which a naive fingerprint-only check will flag as a false collision. Territory-scoped re-releases (a single master reissued under a new code for a specific market) are not collisions at all and should be excluded before this logic runs; conflate the two and you will quarantine healthy records. Deleted-and-recycled ISRCs — where a registrant reuses a code retired years earlier — are the hardest case, since both recordings can be genuinely valid under the agency’s own history; these should always route to manual review regardless of timestamp, never to earliest-registration auto-resolve. Finally, any override of an automated decision must go through role-gated access rather than a direct database write, consistent with the controls described in Security Boundaries for Royalty Data.

Collision resolution assumes upstream metadata has already been normalized; when the same recording instead reports different field values across storefronts rather than a reused identifier, that is the narrower problem covered in How to Resolve Conflicting ISRCs Across DSPs. Both feed the same ISRC-to-ISWC Mapping Workflows reconciliation stage before splits are finalized.