Spotify Catalog Resolver: Bulk ISRC & UPC Music Lookup avatar

Spotify Catalog Resolver: Bulk ISRC & UPC Music Lookup

Pricing

Pay per event

Go to Apify Store
Spotify Catalog Resolver: Bulk ISRC & UPC Music Lookup

Spotify Catalog Resolver: Bulk ISRC & UPC Music Lookup

Bulk ISRC & UPC lookup and text search over a pre-indexed 2025-07 music catalog snapshot: 256M Spotify tracks, 58M albums, 15M artists. One metadata row per match. ISWC is a separate work-level mode, no observed coverage. Independent index, not affiliated with Spotify.

Pricing

Pay per event

Rating

0.0

(0)

Developer

Paul Mikulskis

Paul Mikulskis

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

Resolve ISRCs, UPCs, or free-text queries to full Spotify catalog metadata (Spotify IDs, artists, album, cover art, popularity, release date) in a single call, over a pre-indexed 256M-track / 58M-album / 15M-artist snapshot. One identifier returns its release variants, up to the limit you request and never more than 50 rows per value; a recording with more variants than that is truncated, not exhausted. There is no Spotify OAuth workflow to complete and no Web API rate wall to work around.

ISWC lookup is a separate, work-level mode. It does not run against the Spotify snapshot, returns a different row shape, and no coverage has been observed for it. Read ISWC lookup before choosing it.

What does Spotify Catalog Resolver do?

Spotify Catalog Resolver takes a list of identifiers or search terms and returns the matching Spotify catalog entities as structured rows. It runs against a purpose-built, pre-indexed Postgres full-text index of a Spotify metadata snapshot, so it answers ISRC → track, UPC → album, and text → track/artist/album lookups from a warm index without putting each lookup through the Spotify Web API workflow.

Because it runs on the Apify platform, you get API access, scheduling, integrations (Make, Zapier, n8n), monitoring, and downloadable datasets out of the box.

Why use Spotify Catalog Resolver?

  • Release variants in bulk, from one index, up to your limit and never more than 50 per value. This is the part that holds up against free alternatives: probed head to head on 2026-08-12, Deezer and MusicBrainz each resolved the same ISRC unauthenticated and returned a single result, while this index filled a limit of 10. Ten was the cap that call asked for, not a total: that ISRC's actual snapshot count is 12, measured by saturation on 2026-08-13. Skipping the Spotify OAuth workflow is a convenience here, not the differentiator.
  • No OAuth workflow or Web API rate-wall dependency. Resolve up to 1,000 identifiers in one run.
  • Bulk catalog matching. Paste a list (or a CSV URL) of ISRCs/UPCs/titles and get the matches back, plus an optional summary of how many resolved and how many did not.
  • Built for music-tech. Rights and royalty tooling, catalog reconciliation, metadata enrichment, playlist/analytics pipelines, and AI music agents.

How to use Spotify Catalog Resolver

  1. Pick a Query mode (e.g. Lookup by ISRC, Search tracks, or Bulk resolve).
  2. Add your values to Queries, one identifier or search phrase per item. For bulk jobs you can instead point CSV URL at a public CSV (the first column is used).
  3. Set Max rows per query (1 to 50) to cap how many hits each value returns.
  4. Click Start. Results stream into the dataset, one row per match.
  5. Download the dataset as JSON, CSV, Excel, or HTML, or pull it from the API.

Input

Configure the run in the Input tab.

FieldDescription
modelookupIsrc, lookupUpc, lookupIswc, searchTracks, searchArtists, searchAlbums, typeahead, or bulk.
queriesValues to resolve: one ISRC/UPC per item, or free text for search modes.
Run sizeA run processes at most the first 1,000 values. Anything beyond that is dropped, is not resolved, and is not charged for; the run logs how many were dropped and OUTPUT.truncated records that it happened. Split larger lists across runs.
csvUrlBulk mode only: a public CSV URL; the first column of each row is used as a query value. When set, it overrides queries. If that first cell is exactly isrc, upc, iswc, query, name, id, code, value or title, ignoring case, the row is treated as a header and skipped, so a real value spelled that way is dropped. The response must be 5 MiB or smaller, at most 5 redirects are followed, and non-public destinations (private, loopback and link-local addresses) are refused. Only the first 1,000 values are processed.
limitMax hit rows returned per input value, 1 to 50. Default 10. Each returned row is a charged match, so this is the main cost control. Ignored by lookupUpc, which returns at most one album per UPC.
typesTypeahead only: restrict to track, artist, and/or album.
includeReportBulk only: add one summary row with matched / not-found counts. Free; see Pricing.

Example input:

{
"mode": "bulk",
"queries": ["USUG12001402", "GBUM71029604", "00602507122591"],
"limit": 1,
"includeReport": true
}

Output

Each match is one dataset row, and one identifier usually matches many rows: an ISRC appears on every release that carries the recording, and you get one row per release variant, up to limit rows.

This is the single most important thing to understand about cost. USUG12001402 matches 12 release variants in the 2025-07 snapshot, measured by raising limit until the count stopped growing (12 at limit 13, 20 and 50 alike). At the default limit of 10 you therefore get 10 charged rows, $0.04 for one input value, not one row for $0.004. Set limit: 1 if you only want the canonical match; raise it only if you want variants.

Some recordings have more variants than you can request at once. GBUM71029604 ("Bohemian Rhapsody") returns a full 50 rows at limit: 50, the schema maximum, and its count was still rising at that point, so its true total is at least 50 and not established here. A returned row count equals the cap whenever the cap is the binding constraint, so never read one as a variant total.

The row below is the first of those variants, copied verbatim from the public demo dataset linked below, so you can diff it against real output:

{
"query": "USUG12001402",
"mode": "bulk",
"matchedVia": "lookupIsrc",
"status": "matched",
"type": "track",
"spotifyId": "68kQQv6idXUcXIf1n59Y3i",
"spotifyUrl": "https://open.spotify.com/track/68kQQv6idXUcXIf1n59Y3i",
"isrc": "USUG12001402",
"name": "Blinding Lights - Major Lazer Remix",
"albumName": "Blinding Lights (Major Lazer Remix)",
"albumUpc": "00602507122591",
"artistNames": ["The Weeknd", "Major Lazer"],
"artistSpotifyIds": ["1Xyo4u8uXC1ZmMpatF05PJ", "738wLrAtLtCtFOLvQBXOXp"],
"coverUrl": "https://i.scdn.co/image/ab67616d0000b273cddd966766ba3b2bc20fa991",
"coverWidth": 640,
"coverHeight": 640,
"durationMs": 197961,
"popularity": 43,
"releaseDate": "2020-03-31",
"snapshotId": "2025-07"
}

The next variant is the same recording on a different release (albumUpc 00602435248820). Every variant returned is a charged row.

Every value the run attempts produces a row, and any row that is not a match is free. Four statuses are free: "not_found" when the catalog has no entry, "invalid" when the identifier is malformed, "upstream_unavailable" when the catalog index could not be reached, and "error" when that value failed for some other reason. "upstream_unavailable" appears on a single row when the index is unreachable before the run starts, and on one row per affected value when it goes away mid-run. That single pre-start row carries requestedInputs, the number of values that would have been resolved, because no per-value rows exist on that path. Only "matched" rows are charged as resolved-row.

Do not treat row count as input count. Values and rows are different populations, and conflating them understates your bill:

  • every attempted value produces at least one row;
  • an attempted value that matched produces one charged resolved-row per release variant, up to limit. That is where the cost is: at the default limit of 10, the worked example above produces 10 charged rows, $0.04, from a single input value;
  • an attempted value that ended in any free status (not_found, invalid, error, upstream_unavailable) produces exactly one row, and it is free.

To bound spend, bound limit; bounding the number of input values is not enough.

Three counts in OUTPUT close the books on the values themselves, and they are three different numbers:

  • receivedInputs: how many values you supplied.
  • requestedInputs: how many were submitted to the run after the 1,000 cap. Values past the cap are dropped before the run sees them: never submitted, never resolved, never charged for. This is not how many were processed either, because a run that stops early submits more than it attempts.
  • skippedInputs: how many were submitted but never attempted, because the run stopped. That happens when the charge budget runs out or a charge call fails, when the catalog index goes away mid-run and the remaining work cannot pay for itself, or when the run never starts resolving, because the index was unreachable at preflight and no value was attempted at all. Nothing is charged for these.

So the value accounting is exact, and it is not the row accounting: receivedInputs - requestedInputs were dropped at the cap, and requestedInputs - skippedInputs were attempted, each with at least one row of its own.

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

Demo dataset and catalog-match report

The full multi-input demo is free to inspect, no run required. It was generated locally in bulk mode against the live snapshot from 3 identifiers: 2 real ISRCs and 1 synthetic probe not in the catalog.

  • Dataset (6 total rows: 5 result rows plus 1 catalog-match-report row): https://api.apify.com/v2/datasets/jTIj4w70lvyT0HyAg/items?format=json&clean=true. This run was made with limit: 2, so each matched identifier returns two rows here. That is the cap it ran under, not the number of variants that exist: both identifiers match far more in the snapshot. USUG12001402 (The Weeknd, "Blinding Lights - Major Lazer Remix") and GBUM71029604 (Queen, "Bohemian Rhapsody") each return two rows in this demo, and ZZZZZ0000001 (a synthetic, non-existent ISRC) comes back not_found.
  • Catalog-match report: same dataset, the row with "recordType": "catalog-match-report", a sample of the summary artifact includeReport: true produces. Over these 3 identifiers: 2 matched, 1 not-found, 4 rows resolved, at that run's limit: 2. It was captured on 2026-07-11 and is not byte-identical to what a run produces today: the record has since gained receivedInputs, the count of values you supplied before the 1,000 cap. Note the naming: the report calls the after-cap count totalInputs, which is the same quantity OUTPUT calls requestedInputs. Every field shown is still produced; one more is added. It was a paid $15 event when this sample was captured and was withdrawn from sale on 2026-08-13; see Pricing.

The dataset and report above are real output from the shipped resolver (a local, non-billing bulk run), not mocked. An earlier two-identifier demo is still up at https://api.apify.com/v2/datasets/237XTcWzSgNldbitT/items?format=json&clean=true: 4 matched rows, two per identifier for USUG12001402 (The Weeknd) and GBUM71029604 (Queen), and no report row. That run was also capped at two rows per value, so like the dataset above it shows the cap rather than how many variants each identifier has.

  • MLC Works Resolver resolves ISRCs to MLC songwriter/publisher rights and royalty shares. Use it for the rights layer; this actor is catalog metadata, not rights.

Data fields

FieldApplies toDescription
spotifyId / spotifyUrltrack / artist / albumSpotify entity ID and open.spotify.com link. Work rows from lookupIswc have neither.
isrctrackInternational Standard Recording Code.
albumUpc / upctrack / albumUniversal Product Code (barcode).
nametrack / artist / albumTrack / artist / album name. Work rows carry primaryTitle instead.
artistNames / artistSpotifyIdstrack / albumCredited artists and their Spotify IDs.
albumNametrackParent album.
coverUrl / coverWidth / coverHeighttrack / albumCover art.
durationMs / popularity / releaseDatetrackTrack detail.
genres / followersTotal / portraitUrlartistArtist detail.
albumType / label / totalTracksalbumAlbum detail.
matchedViamatched / not_found / invalidWhich lookup the value was routed to, useful in bulk and typeahead where the route is auto-detected per value. On a miss it names the route that was tried, not a lookup that answered. Absent on error and upstream_unavailable rows, which fail before a route resolves.
iswc / primaryTitle / isrcs / writers / publishers / recordingsworkISWC lookup only. These come from the work-level crosswalk, not the Spotify snapshot.

ISWC lookup (work-level, no observed coverage)

lookupIswc is not a Spotify-snapshot lookup and does not return Spotify catalog metadata. It queries a separate work-level crosswalk and returns work rows: iswc, primaryTitle, isrcs, writers, publishers, recordings, and a confidence score. There is no spotifyId, no cover art, no popularity and no release date on these rows, and they are not covered by the 256M-track snapshot figure quoted above.

No ISWC coverage has been observed, and that is measured rather than estimated. Five real ISWCs, including well-known works, were probed against the live index on 2026-08-13, each at limit 5, and every one returned zero rows, while an ISRC control returned full track rows in the same session. Five probes is a small sample and it is the whole sample: no ISWC has been observed resolving at all. Misses cost nothing, so trying it is free, but do not plan a workflow around ISWC resolution on the strength of the other modes. Use lookupIsrc when you have a recording identifier.

Pricing

This actor is pay-per-event:

  • resolved-row: $0.004 per matched row pushed to the dataset.
  • Actor start: $0.00001 per event, charged one event per GB of run memory with a minimum of one event. At the 512 MB default the minimum applies, so it is $0.00001 per run; a 4 GB run is four events, $0.00004. The platform bills this itself at start, on every run, including runs that fail before resolving anything.

Exactly two things can charge you, and nothing else does:

  1. Each matched row, at $0.004. One input can produce several matched rows, and each one is charged. Resolving 1,000 ISRCs that all match at 1 row each costs about $4.
  2. The start event, $0.00001 at the default memory, on every run, including failed ones. This one is billed by the platform rather than by the actor, so it is the one charge a run cannot avoid.

Free, always: not-found rows, invalid identifiers, values that could not be resolved, rows written when the catalog index was unavailable, and values the run never attempted. A bulk job with misses never bills you for the misses.

The catalog-match summary is free

The catalog-match summary is free. includeReport: true adds one summary row to a bulk run and costs nothing, so there is no budget to run out of and no case where it is withheld for money reasons. It is emitted whenever a bulk run asks for it and the run gets far enough to produce one; a run that fails at preflight never reaches the report stage at all.

It used to be a $15 charge event. The record carries twelve fields: seven integers, two booleans and three strings. The nine that carry report information are all computable from rows you have already bought and from the input list you supplied. The other three are not: recordType and snapshotId are constants, and generatedAt is the time the summary was built. Those three are real values; they are simply not worth paying for, and none of them is derived from the rows you bought. A worked instance is free to read in the demo dataset linked above, one field short of the current record, and not on the Store card itself. Charging $15 for arithmetic over your own data was not defensible, so it is not charged.

The catalog-match-report event that used to carry that $15 charge was removed from this actor's pricing on 2026-08-13, so it no longer exists to be billed. The only two things that can charge you are the two listed above.

Tips

  • For exact identifier resolution use lookupIsrc or lookupUpc: they are index-backed against the snapshot rather than full-text scans, so what they return is bounded by the identifier rather than by how many rows a phrase happens to match. lookupUpc returns at most one album, so it costs at most one row. lookupIsrc still returns one row per release variant up to your limit, so it is predictable but not automatically cheap. The price per matched row is $0.004 in every mode; the mode changes how many rows a value can produce, not what a row costs. lookupIswc is not covered by any of this; it queries the separate work-level crosswalk described above.
  • Use limit: 1 in bulk mode when you only need the canonical match per identifier (keeps cost predictable).
  • Very common single words (e.g. a bare "the") are expensive full-text queries. If one takes too long the value is not resolved: you get a single free "error" row for it and the run continues. Prefer specific titles or identifiers.

FAQ, disclaimers, and support

  • Where does the data come from? A pre-indexed metadata snapshot (snapshot 2025-07). It is a point-in-time index, not a live mirror of Spotify.
  • Is this affiliated with Spotify? No. Spotify is a trademark of Spotify AB; this actor is an independent metadata-resolution tool and is not endorsed by or affiliated with Spotify.
  • Coverage. Newer releases after the snapshot date may not be present; unmatched inputs return free status rows.
  • Support / custom needs. Use the Issues tab for feedback or to request a custom catalog-resolution solution.