Spotify Artist, Album & Playlist Scraper avatar

Spotify Artist, Album & Playlist Scraper

Pricing

from $1.70 / 1,000 entity delivereds

Go to Apify Store
Spotify Artist, Album & Playlist Scraper

Spotify Artist, Album & Playlist Scraper

Scrape one public Spotify artist, album or playlist URL: artist monthly listeners; album release year, type and track count; playlist item and save counts when Spotify exposes them. Server-rendered card lookup only — no search, tracks, audio features, history, login or API key.

Pricing

from $1.70 / 1,000 entity delivereds

Rating

0.0

(0)

Developer

Tim Zinin

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Spotify Artist, Album & Playlist Scraper: Read Public Spotify Cards by URL

How this Actor turns your input into a decision This Actor takes a Spotify artist, album or playlist URL you already have and scrapes its public card — name, monthly listener count (artist), release year and track count (album), item count and save count (playlist) — straight from the same server-rendered application/ld+json block Spotify's page ships before any client-side JavaScript runs. No login, no API key, no Web Playback SDK, no browser.

This is deliberately narrow, and it runs backwards from almost every other Actor in this line. Most scraping advice says "wear a browser User-Agent so the source doesn't block you." Here, that advice would break the product: Spotify's card page only renders its data server-side for a plain, non-browser request. A real browser signature gets an empty client-side shell — 156,405 bytes of "Spotify – Web Player" with zero data — that expects a real browser to execute JavaScript this Actor never runs. This Actor's request layer is built around exactly one non-negotiable rule: never send a browser User-Agent to open.spotify.com, ever. That rule is a golden in acceptance.json, checked against this run's own outgoing header, independent of what Spotify happens to return that day. See Evidence and boundaries for the live measurement, and the first-pass recon mistake that made this rule necessary to spell out this plainly.

The billable outcome is a delivered card: one row, found:true, where a name was actually extracted from the page's own structured data. Everything else is free: a URL that doesn't resolve to a real Spotify object (found:false, a genuine HTTP 404 from Spotify itself — not a silent-empty result), a non-Spotify host refused before any request is made, and a page that answered HTTP 200 but carried no usable ld+json block. This page shows the real JSON this Actor produced for an artist, an album, a playlist, a blocked host and a genuine 404 — taken from runs made while this page was being written, including five repeated fetches of the same playlist that show a live, disclosed inconsistency in Spotify's own page. See Real happy, partial and failure output.

What you get

  • Artist: name and Spotify's own displayed monthly-listener figure.
  • Album: name, release type, release year and track count.
  • Playlist: name, item count and save count — with an honest disclosure that these last two can legitimately come back empty on the exact same URL across repeated fetches (see below; this is a real, observed property of the source, not a defect in this Actor).
  • The one rule that makes this Actor work at all: it never wears a browser User-Agent. Confirmed on every successful row via the requestUserAgent field, which echoes the literal header this Actor sent — not a claim, a receipt, checked on every single row this build produces.
  • A genuine HTTP 404 is never confused with an empty result. Spotify answers a nonexistent object with a real 404, not a silent HTTP 200 with nothing in it — this Actor checks the HTTP status before it ever tries to parse a page body, so the two failure modes other scrapers in this same hub have to guard against (silent-empty vs. genuine error) are simply not the same risk here. See Evidence and boundaries for the contrast, named explicitly.
  • No login, no API key, no Web Playback SDK, no browser runtime of any kind.
  • Runs on Apify: schedule it, call it from the API, the MCP server, or an n8n/agent workflow, export to JSON/CSV/Excel, or push results straight into your own pipeline.

Who uses it

  • Music analytics and A&R research teams batch-checking artist monthly-listener figures across a watchlist, without opening each artist page in a browser.
  • Agencies tracking client or competitor releases — album metadata (release year, track count) for a defined list of albums, refreshed on a schedule.
  • Playlist and campaign researchers reading item and save counts for editorial or brand playlists they are monitoring, understanding that these two fields are the least stable ones this Actor reports (see Evidence and boundaries) and reading them as a best-effort snapshot, not a guaranteed number.
  • Developers and agent/workflow builders who want one flat JSON row per target — found, type, error — that a script, an n8n node, or an LLM agent can branch on directly, without parsing Spotify's HTML or its application/ld+json shape by hand, and without accidentally breaking extraction by adding a browser-looking header somewhere upstream.
  • Not for buyers who want to search Spotify by artist name, stream or download audio, or pull a full discography. This Actor accepts a URL you already have and reads that one page — see Limits for exactly what is out of scope.

Where this Actor sits in your workflow

How to run

  1. Populate Spotify URLs with one or more full URLs — https://open.spotify.com/artist/<id>, /album/<id> or /playlist/<id>. A bare ID without the URL is not accepted; the type is always read from the URL path, never guessed. Up to 50 per run.
  2. Leave Max concurrency at its default of 5 unless you have a specific reason to raise it (up to 15) or lower it (down to 1).
  3. Start the run from the Console, the Apify API, a scheduled Task, or an MCP client. The prefilled input (a real artist URL) is a verified, live target — a default run with no edits produces one real, non-empty row at essentially no cost, so you can see the actual data shape before committing a longer list.
  4. Read the Dataset: one row per URL you supplied, always in the same 15-field shape regardless of outcome. Check found before anything else — only found:true rows are billed.
  5. Check requestUserAgent on any row where you need to independently confirm this Actor behaved as documented — it is the literal header value this specific run sent, not a static claim on this page.
  6. Check this run's key-value store record OUTPUT for a one-time run summary — requested/delivered/ paid/free/failed/incomplete counts, replay safety and any fatal error — written once at the end of the run. The Dataset itself remains the complete, row-by-row authoritative record; OUTPUT is a roll-up of it.

Pricing

$0.005 per Actor start, plus $0.002 per delivered card. The primary pricing noun on this page, in the Dataset, and in this Actor's platform pricing record is the same throughout: the delivered card — one row where a name was actually extracted from the target's own structured page data (found:true). 100 delivered cards cost about $0.205 ($0.005 start + 100 × $0.002).

Every other row is free: a target that doesn't resolve (a genuine HTTP 404), a non-Spotify host refused before any request, and a page that answered HTTP 200 with no usable application/ld+json block. A run of 50 URLs where 10 are stale links no longer resolving is billed for the up-to-40 it actually delivered, plus the one fixed start fee — never for the 10 it honestly reports as not found.

Both free outcomes are enforced in code, not left to a documentation promise: the charge and the dataset write happen together, inside one mutex-protected step (lib/billing.js), and a row is never pushed to the paid channel unless found is already true at that point. There is no code path in this build where a found:false row is billed.

This Actor's own external data cost is close to zero — one GET request per target, no proxy, no LLM, no paid third-party API — so the per-card price reflects the extraction and the User-Agent discipline described on this page, not a resold data bill.

On Apify's own billing record these are metered as two separate platform events — apify-actor-start for the run fee, and result-found for each delivered card — the same two lines you will see on your invoice.

Input contract

Exactly one field is required.

  • targets — array of strings, 1 to 50 items, required. Each item must be a full URL — https://open.spotify.com/artist/<id>, /album/<id> or /playlist/<id> — with open.spotify.com as its host. The type (artist/album/playlist) is read directly from the URL path; this Actor never guesses it and never accepts a bare ID without the path. A URL whose host is not open.spotify.com is rejected with found:false and an explicit host-mismatch message, before any network request is made — verified live below, not just asserted.
  • maxConcurrency — integer, 1 to 15, default 5. How many targets this run reads in parallel.

Two runnable input examples, built directly from .actor/input_schema.json — these are input illustrations, not run output; the Real happy, partial and failure output section below shows the actual Dataset rows these kinds of inputs produced, from real runs of this exact build.

{
"targets": ["https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02"],
"maxConcurrency": 5
}

(This is the Actor's own prefilled default input — a real, verified artist URL, run at the default concurrency. This is what a buyer sees and can run with zero edits.)

{
"targets": [
"https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02",
"https://open.spotify.com/album/4yP0hdKOZPNshxUOjY0cZj",
"https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M",
"https://open.spotify.com/artist/00000000000000000000ZZ"
],
"maxConcurrency": 1
}

(Mixes all three supported object types with one deliberately nonexistent artist id in the same run — to show that a buyer does not need to pre-filter their own list before sending it in; the nonexistent one simply comes back as a free, honest found:false row with a real HTTP 404 behind it, alongside the three real results. All four URLs in this example are the same ones this page's live evidence was gathered against.)

Real happy, partial and failure output

Every row in the Dataset shares one flat, 15-field shape regardless of outcome — input, found, type, spotifyId, canonicalUrl, name, releaseType, releaseYear, trackCount, monthlyListeners, itemCount, savesCount, error, requestUserAgent, checkedAt. Every JSON block below is copied verbatim from an actual local run of this exact main.js build (node main.js with CRAWLEE_STORAGE_DIR set to a local storage folder), executed while this page was being written, against the live Spotify site. Nothing here is invented or reconstructed from the schema — every field value below is what the Dataset actually contained after that run finished.

Happy path — a real artist card, real monthly-listener figure

{
"input": "https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02",
"found": true,
"type": "artist",
"spotifyId": "06HL4z0CvFAxyc27GXpf02",
"canonicalUrl": "https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02",
"name": "Taylor Swift",
"releaseType": null,
"releaseYear": null,
"trackCount": null,
"monthlyListeners": "100.9M",
"itemCount": null,
"savesCount": null,
"error": null,
"requestUserAgent": "Mozilla/5.0 (compatible; spotify-artist-intel/0.1; +https://apify.com/zinin/spotify-artist-intel)",
"checkedAt": "2026-08-17T20:32:57.777Z"
}

requestUserAgent is the literal header this run sent — a plain, declared, non-browser string with no AppleWebKit, no Chrome/, no Safari/ token anywhere in it. This is not a static claim: it is read back from this exact run, and this Actor's acceptance suite fails immediately if this field ever contains a real browser signature, independent of what Spotify's page happened to return that day.

Happy path — a real album card, release year and track count

{
"input": "https://open.spotify.com/album/4yP0hdKOZPNshxUOjY0cZj",
"found": true,
"type": "album",
"spotifyId": "4yP0hdKOZPNshxUOjY0cZj",
"canonicalUrl": "https://open.spotify.com/album/4yP0hdKOZPNshxUOjY0cZj",
"name": "After Hours",
"releaseType": "album",
"releaseYear": 2020,
"trackCount": 14,
"monthlyListeners": null,
"itemCount": null,
"savesCount": null,
"error": null,
"requestUserAgent": "Mozilla/5.0 (compatible; spotify-artist-intel/0.1; +https://apify.com/zinin/spotify-artist-intel)",
"checkedAt": "2026-08-17T20:32:58.331Z"
}

Both rows above are billed, delivered cards — found:true, real names, real type-specific fields.

Partial/disclosed path — the same playlist, five repeated fetches, two different honest shapes

This is the single most important thing to understand before buying playlist data from this Actor. Spotify's own playlist page does not consistently carry the structured "N items · N saves" text this Actor's parser reads. Five back-to-back fetches of the exact same URL, https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M, made in immediate succession while writing this page, produced three rows with the structured counts present and two rows where they came back null — on the identical URL, the identical request, seconds apart:

{
"input": "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M",
"found": true,
"type": "playlist",
"spotifyId": "37i9dQZF1DXcBWIGoYBM5M",
"canonicalUrl": "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M",
"name": "Today’s Top Hits",
"releaseType": null,
"releaseYear": null,
"trackCount": null,
"monthlyListeners": null,
"itemCount": 50,
"savesCount": "33.9M",
"error": null,
"requestUserAgent": "Mozilla/5.0 (compatible; spotify-artist-intel/0.1; +https://apify.com/zinin/spotify-artist-intel)",
"checkedAt": "2026-08-17T20:33:07.527Z"
}

The fourth fetch of the same URL, roughly two seconds later, with found and name still both correct, but itemCount/savesCount both honestly null:

{
"input": "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M",
"found": true,
"type": "playlist",
"spotifyId": "37i9dQZF1DXcBWIGoYBM5M",
"canonicalUrl": "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M",
"name": "Today’s Top Hits",
"releaseType": null,
"releaseYear": null,
"trackCount": null,
"monthlyListeners": null,
"itemCount": null,
"savesCount": null,
"error": null,
"requestUserAgent": "Mozilla/5.0 (compatible; spotify-artist-intel/0.1; +https://apify.com/zinin/spotify-artist-intel)",
"checkedAt": "2026-08-17T20:33:09.912Z"
}

Both rows are billed — found:true is the only billing gate this Actor has, and both rows genuinely found the playlist and extracted its name. The difference between them is not a parsing failure on this Actor's side; it is Spotify's own page inconsistently carrying the structured description text this parser depends on for the count fields specifically. See Evidence and boundaries for the full accounting of all five fetches and why this Actor does not treat a null count as an error.

Failure path — a non-Spotify host, refused before any request

{
"input": "https://evil.example/artist/fake",
"found": false,
"type": null,
"spotifyId": null,
"canonicalUrl": null,
"name": null,
"releaseType": null,
"releaseYear": null,
"trackCount": null,
"monthlyListeners": null,
"itemCount": null,
"savesCount": null,
"error": "host must be open.spotify.com — refusing to request \"evil.example\"",
"requestUserAgent": null,
"checkedAt": "2026-08-17T20:32:59.384Z"
}

requestUserAgent is null here, not the actor's declared string — this is deliberate confirmation that no request was ever built or sent for this row, not just that the response was discarded.

Failure path — a genuine HTTP 404, not a silent-empty result

{
"input": "https://open.spotify.com/artist/00000000000000000000ZZ",
"found": false,
"type": "artist",
"spotifyId": "00000000000000000000ZZ",
"canonicalUrl": null,
"name": null,
"releaseType": null,
"releaseYear": null,
"trackCount": null,
"monthlyListeners": null,
"itemCount": null,
"savesCount": null,
"error": "http 404",
"requestUserAgent": "Mozilla/5.0 (compatible; spotify-artist-intel/0.1; +https://apify.com/zinin/spotify-artist-intel)",
"checkedAt": "2026-08-17T20:32:59.385Z"
}

requestUserAgent is populated here, unlike the blocked-host row above — this request genuinely reached Spotify's servers and Spotify itself answered 404. Both rows are free, but for different reasons: one never left this Actor's process, the other was refused by the source.

Field dictionary

FieldMeaningImportant boundary
inputExactly the URL you supplied, unmodifiedEchoed even when the value was rejected before any request
foundThe billing boundarytrue only when a name was actually extracted from the page's own ld+json data
typeartist / album / playlist, read from the URL pathSet as soon as the URL parses, even on a row that later fails (e.g. the 404 example above still carries type:"artist")
spotifyIdThe object's id, taken from the URLSame availability as type
canonicalUrlThe @id/url field from the page's own structured data, or the request URL as a fallbacknull when no data was extracted
nameTitle of the artist, album or playlistnull when found:false
releaseType / releaseYear / trackCountAlbum-only fieldsnull for artist/playlist rows, and null for an album row if the source's description text didn't carry that specific piece
monthlyListenersArtist-only — Spotify's own displayed figure (e.g. "100.9M")Not an exact internal count; a display string, not a number
itemCount / savesCountPlaylist-onlyCan legitimately be null even on a found:true row — see the five-fetch evidence above; this is the source's own inconsistency, never invented when absent
errorReason for found:falsenull on every found:true row; states the real HTTP status or the local validation reason
requestUserAgentThe literal User-Agent header this run sent to Spotifynull only when no request was made at all (blocked-host row); otherwise always this Actor's fixed, declared non-browser string — checked, never a browser signature, by this build's own acceptance suite
checkedAtISO-8601 timestamp of this row's outcomePer-row, not per-run

Evidence and boundaries

The core mechanic, measured both directions. A bare (non-browser) request to open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02 returns HTTP 200, roughly 260 KB, with a MusicGroup application/ld+json block containing "100.9M monthly listeners." The identical URL requested with a real Chrome User-Agent returns HTTP 200, roughly 156 KB, and zero ld+json blocks — an empty client-side shell titled "Spotify – Web Player" that expects a real browser to run its JavaScript and never gets one. This Actor's request layer sends the first kind of request, always, and never the second — confirmed on every successful row above via requestUserAgent.

Why this rule gets its own recon subsection: a first-pass mistake, disclosed plainly. The first attempt at researching this source rejected the idea outright — "no server-side content here" — because every one of five separate test requests carried a browser User-Agent. Five repeats of one broken test were mistaken for five independent confirmations of an absence. One single bare curl, with no -A flag at all, immediately returned the full 260 KB of real data. The specific failure mode is worth naming precisely: it is not that the earlier tests were wrong about what they measured, it's that all five were the same corrupted measurement repeated, not five different ones. This is why acceptance.json's golden #2 checks this Actor's own outgoing User-Agent header directly, as an invariant independent of any single day's response — a future edit that "helpfully" swaps in a browser signature (the normal move almost everywhere else in this fleet) fails immediately, regardless of what Spotify's page happens to return that day.

The playlist instability, the full accounting of all five fetches, not just two. Five consecutive requests to the identical URL (https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M), made back-to-back while writing this page: fetch 1 returned the structured "50 items · 33.9M saves" form; fetch 2, structured; fetch 3, structured; fetch 4, the bare curator-blurb form with itemCount/savesCount both null; fetch 5, also the blurb form. Three of five structured, two of five not — on the same URL, same request shape, seconds apart, no code change between fetches. found and name succeeded on all five without exception. This Actor's own extraction code does not distinguish "the structured text is momentarily absent" from any other description-text shape — it reads whichever description Spotify's page served that specific request, and reports honestly whatever it finds, including nothing, for the two count fields specifically. This is consistent with, but not identical in ratio to, an earlier recon note that reported four-of-five structured on a different set of fetches — the point both measurements agree on is that this instability is real and repeatable, not a one-off; the exact ratio on any given attempt is not something this Actor promises to reproduce.

Why null counts on a playlist are never treated as an error. Given the instability measured above, this Actor's design choice is to let itemCount/savesCount be honestly empty rather than either (a) guessing a value from a cached prior fetch, or (b) marking the whole row found:false because two of thirteen fields didn't populate. A buyer who needs those two specific fields for a specific playlist should expect to re-run this Actor and treat a null result as "try again," not as "this playlist has no saves."

The negative case, honest and unambiguous — contrasted deliberately with a sibling Actor's source. A syntactically well-formed but nonexistent artist id (open.spotify.com/artist/00000000000000000000ZZ) returns a genuine HTTP 404, roughly 6 KB, with zero ld+json blocks — checked and recorded via the HTTP status alone, before any attempt to parse a body. This is a meaningfully different shape from the "silent-empty" pattern this same hub's YouTube-playlist Actor has to guard against, where a nonexistent playlist id returns HTTP 200 with a real page and zero results. Spotify does not do that for a nonexistent artist/album/playlist id — it answers with an honest 404. This Actor's own code does not carry over the "200 might mean empty" suspicion from that sibling Actor's source; the two sources genuinely behave differently, and pretending otherwise here would be inventing a defense against a failure mode this specific source does not produce.

The SSRF/host guard, confirmed live, not just read from source. The blocked-host example above shows the real outcome of a targets value pointing at a non-Spotify host: found:false, an explicit host-mismatch message, and requestUserAgent:null — proof no request was built or sent, not just that a response was discarded. Every target's host is checked against open.spotify.com before any network call is made, and the DNS-rebinding/private-address pinning logic in main.js (resolveVerifiedAddresses) guards the one real request path as defense in depth, following the same pattern used across this hub's other Actors.

Rights posture toward AI training crawlers, named plainly, not omitted. open.spotify.com/robots.txt grants Allow: / under the general User-agent: * rule (excluding only /local/, /download/ and /embed/, none of which this Actor ever requests) — but separately, and by name, issues Disallow: / to GPTBot, anthropic-ai and ClaudeBot. This Actor does not identify itself as any of those crawlers and is not one — it sends its own declared, non-browser identifier string, and operates strictly under the general User-agent: * rule. The platform's position is stated here without softening: Spotify permits ordinary crawling of these public card pages, and separately, explicitly does not want its content used to train AI models. This Actor's product is the publicly displayed metadata of one page at a time — a name, a listener count, a track count — delivered to a buyer who supplied the URL; it is not, and does not present itself as, a corpus for model training.

Decision routing

Row shapeBillable?What it means
found:true, all relevant fields populatedYesThe full card was read cleanly
found:true, playlist with itemCount/savesCount both nullYesName was extracted; the two count fields were honestly absent from Spotify's own page on this specific fetch — see Evidence and boundaries
found:false, non-Spotify hostNoRefused locally before any request; requestUserAgent:null proves no request was sent
found:false, genuine HTTP 404NoSpotify itself answered "this object does not exist" — not a silent-empty result
found:false, HTTP 200 but no usable ld+jsonNoA possible page-format change on Spotify's side; reported honestly, never guessed at

A workflow needs one rule to be correct: bill and use only found:true rows, and for playlist data specifically, treat a null itemCount/savesCount on an otherwise successful row as "not available on this fetch," not as a defect to report.

Commercial playbooks

Artist watchlist for A&R or catalog teams

An analyst maintains a list of artist URLs (prospects, catalog artists, competitors) and runs this Actor on a schedule to track the monthly-listener figure Spotify itself displays, without opening each artist page by hand.

Release-metadata tracking for agencies

An agency tracking a client's or a competitor's discography feeds in album URLs as they release, pulling release year and track count into a catalog record without re-typing it from the page.

Playlist and campaign monitoring

A brand or campaign team tracking a specific editorial or owned playlist's item and save counts over time, understanding upfront (per Evidence and boundaries) that these two fields can legitimately come back empty on any given fetch and planning a retry-tolerant pipeline around that fact rather than treating a single null result as a broken run.

Integration recipes

One-shot batch lookup

Feed a list of artist/album/playlist URLs collected from any source — a spreadsheet, a search result set, a manual list — into one run with maxConcurrency set to match the batch size (up to 15). Filter the resulting Dataset to found:true before loading it into a catalog or spreadsheet.

Agent/MCP pattern

An agent that already has a specific Spotify URL (via search, a page it read, or a user-supplied link) passes it directly into targets, checks found before treating any row as usable, and for playlist rows specifically, does not treat a null itemCount/savesCount alongside a real name as a failure to retry immediately — it is a valid, disclosed outcome of this specific source's own inconsistency.

Spreadsheet/catalog append pattern

Dataset rows are appended keyed by spotifyId for found:true rows, and by input + checkedAt for found:false rows (which carry no reliable spotifyId on a blocked-host row). null fields are preserved as null, not coerced into an empty string, so a downstream query can tell "Spotify did not show this field on this fetch" apart from "this field was never populated."

Operating guide

Never add a browser User-Agent upstream of this Actor. This is the single most important operating note on this page. If you fork this code, wrap it in a proxy that rewrites headers, or route it through any tooling that "normalizes" outgoing requests to look like a browser, this Actor's entire data path breaks silently — you would get HTTP 200 responses with zero usable data, not an error. The requestUserAgent field on every row exists specifically so you can verify this did not happen to your own run.

Reading a playlist's null counts correctly. A found:true playlist row with itemCount:null and savesCount:null is not a partial failure to alert on — it is Spotify's own page not carrying the structured description text on that specific fetch. If your use case needs those two fields reliably, build a re-fetch into your pipeline rather than treating a single null result as final; the five-fetch measurement above showed 3-of-5 structured on one run, which is a real ratio to plan around, not a guarantee to expect exactly.

Choosing maxConcurrency. The default of 5 is a conservative starting point. Raising it (up to 15) speeds up a run with many targets; it does not change what any individual target's request returns, since each target is fetched with exactly one request regardless of concurrency.

Telling a blocked host apart from a genuine 404 in your own code. Check requestUserAgent: null means this Actor never sent a request for that row (host guard fired); a populated value alongside found:false means the request reached Spotify and Spotify itself answered negatively (usually a 404). Both are free, but only the second one means "Spotify says this object doesn't exist."

Related tools for adjacent workflows in social platforms and regional directories.

ActorWhat it does
YouTube Channel IntelPair it in the social platforms and regional directories workflow: Public intel card for any YouTube channel: subscribers, total views, video count, join date, country, full...
YouTube Playlist ScraperPair it in the social platforms and regional directories workflow: Scrape any YouTube playlist by id or URL — including other people's public playlists, not just your own...
YouTube Channel Lookalike FinderPair it in the social platforms and regional directories workflow: Find the similar/lookalike channels YouTube itself shows on any channel's About page: channelId, name,...
Bluesky Profile ScraperPair it in the social platforms and regional directories workflow: Batch-fetch public Bluesky profiles by handle or DID via the official keyless AppView API — display name,...
Substack Publication ScraperPair it in the social platforms and regional directories workflow: Read recent posts from any Substack publication's own domain — title, date, full HTML body, comment count...

FAQ

Why does this Actor not use a browser User-Agent?

Because Spotify's card page only renders data server-side for a plain, non-browser request. A real browser signature gets an empty client-side shell that expects JavaScript this Actor never runs — the data disappears entirely. This is the opposite of most scraping advice, and it's deliberate — see Evidence and boundaries for the measurement in both directions, and the recon mistake that made this worth stating this plainly.

Why did my playlist row come back with itemCount/savesCount set to null?

Because Spotify's own page did not carry the structured "N items · N saves" description text on that specific fetch — a live, repeated instability measured directly while writing this page: three of five back-to-back fetches of the same URL had the structured text, two did not. found/name still succeed in both cases; this row is still billed. Re-running the same target is a reasonable next step if you need those two fields specifically.

Does this Actor search Spotify by name?

No — give it the exact artist/album/playlist URL, and it reads that page. Spotify's own search isn't part of this Actor; see Limits.

What happens if the object doesn't exist?

A free, honest row: found:false, error naming a real HTTP 404 from Spotify itself. This is not a silent-empty result — see Evidence and boundaries for why this source behaves differently here than a sibling YouTube Actor in the same hub.

What this Actor is NOT.

It does not stream or download audio, does not list a full discography, and does not read every track in a playlist — only the card-level summary shown on the page itself. It does not search by artist name.

Found a wrong result, or need a check this Actor doesn't run? Open an issue on this Actor's page.

Sources and rights

The pages this Actor reads are reachable by a plain, unauthenticated request, no browser session — that is precisely why it sends a non-browser User-Agent rather than pretending to be a browser.

The only source. The public HTML page at https://open.spotify.com/artist/<id>, /album/<id> and /playlist/<id>, fetched with a plain, declared, non-browser User-Agent. No Spotify Web API, no OAuth, no login session, no Web Playback SDK.

robots.txt, checked fresh, not cached. open.spotify.com/robots.txt: User-agent: *Allow: /, with only /local/, /download/ and /embed/ disallowed — none of which this Actor ever requests. Separately, and by name, GPTBot, anthropic-ai and ClaudeBot are each issued

Disallow: /
. This Actor is not any of those crawlers, identifies itself with its own declared, non-browser string, and operates strictly under the general User-agent: * rule.

The AI-training posture, stated without softening. Spotify's own robots.txt explicitly closes the door to named AI-training crawlers while leaving general crawling open. This Actor's product is the publicly displayed metadata of individual pages — a name, a listener count, a release year, an item count — delivered one target at a time to a buyer who already supplied the URL. It is not a bulk corpus export and does not present itself as training data.

No explicit reuse licence — unlike a government open-data feed. Spotify publishes no equivalent of an Open Government Licence or a CC0 grant for the content of its card pages. This Actor's engineering posture is limited to what robots.txt permits to be crawled and what a genuinely non-browser request receives; it does not constitute legal advice about how you may use the resulting data downstream, and this page makes no claim of an explicit redistribution licence the way a government open-data source would carry one. If your use case depends on a specific reuse right beyond what a robots.txt-permitted, non-browser fetch implies, that determination is yours to make, not this Actor's to certify.

Limits

  • Reads exactly three object types — artist, album, playlist — from exactly one host, open.spotify.com. No track pages, no user-profile pages, no podcast/episode pages.
  • No search by artist, album or playlist name. A full URL is required; the type is read from the URL path, never guessed, and a bare ID without a URL is rejected.
  • Playlist itemCount/savesCount can legitimately be null even on a found:true row — a live, measured instability in Spotify's own page (3 of 5 repeated fetches structured, 2 of 5 not, on the identical URL). This is not a parsing defect; it is disclosed here precisely so a buyer does not mistake it for one. See Evidence and boundaries for the full accounting.
  • monthlyListeners/savesCount are Spotify's own rounded display text (e.g. "100.9M"), never an exact internal number.
  • No audio, no streaming, no preview clips — out of scope and out of the rights this Actor operates under (/embed/ is disallowed by robots.txt).
  • No full discography listing — an artist row gives the card-level snapshot only, not every release.
  • No pagination of playlist contents — this Actor reads the card summary (item count, save count), not the list of tracks inside a playlist.
  • This Actor sends a fixed, deliberately non-browser User-Agent on every request, by design — anything in your own infrastructure upstream of this Actor that rewrites or normalizes outgoing headers toward a browser signature will silently break extraction, not raise an error. See Operating guide.
  • This Actor makes no claim about downstream redistribution rights over fetched card data beyond what is stated under Sources and rights — that determination is the buyer's to make.

Support boundary

Actor support covers this Actor's own input validation, the User-Agent invariant, the SSRF/host guard, the honest-404 negative case, and the Dataset schema described on this page — all backed by a live, verified run referenced above. It does not cover whether a specific artist, album or playlist is a good research target, does not restore access if Spotify changes this page's markup or its anti-bot posture in a way that breaks extraction, does not guarantee a specific ratio of structured-vs-blurb playlist description text on any given fetch (a disclosed, measured instability, not a bug this Actor can fix), and does not provide legal guidance on how fetched card data may be reused downstream. When reporting a problem, include the run id, the exact input URL, the found/error returned, and the checkedAt timestamp of the affected row.