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