A snapchat scraper organised around one question: which selector will still work after Snapchat's next deploy. Built on ScrapingBee's web scraping API.
Snapchat's public profile pages are a React build with CSS module class names, which means most of what a browser inspector hands you is guaranteed to break. This README ranks every extraction target on that page from unusable to permanent, with live responses at each rung.
Verified against snapchat.com/add/teamsnapchat and snapchat.com/add/mrbeast on 2026-09-10.
Copy a class out of devtools on a Snapchat profile and you get this:
UserDetailsCard_title__K9Awz
Heading_h400Emphasis__SQXxl
MediaCard_container___joM8
DesktopUserProfile_desktopContainer__UwOc_
The suffix after the double underscore is a build hash. It changes when the component changes, when the bundler version changes, and sometimes when nothing visible changes at all. A scraper keyed on UserDetailsCard_title__K9Awz works today and returns empty strings after the next release, silently, with a 200 status code. Do not build on rung 0.
Headings describe document structure rather than build output, so they are a real step up from rung 0. The catch is that Snapchat serves two different public profile layouts, and they disagree about which heading holds the name. Both handles below were fetched with the same rule set:
| Selector | teamsnapchat (basic card) |
mrbeast (creator profile) |
|---|---|---|
h1 span |
empty | MrBeast |
h4 span |
Team Snapchat |
empty |
h5 span |
teamsnapchat |
wrong node, the page has ten h5 elements |
h5 + div |
Add me on Snapchat! |
empty |
They are mutually exclusive, so the fix is to request both h1 span and h4 span and take whichever comes back populated. A scraper written against only one of them works on half of Snapchat and returns empty strings on the other half, with a 200 status.
Do not take the username from h5 span. It is correct on the basic card and matches a content tile on the creator profile. Rung 3 has a better source.
Snapchat ships data-testid attributes for its own test suite. They are stable by intent, because their own CI breaks when they move:
[data-testid="snapCodeImage"] -> https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable
That is the Snapcode SVG, parameterised by username, which means you can construct it yourself for any handle once you know the pattern.
These tags exist so Facebook, X and Google can render a preview card. Snapchat cannot change them without breaking every share link on the internet, so they are the closest thing to permanent on this page.
The canonical URL is the best source for the username, because it is byte identical across both layouts and does not change with locale. It always reads https://www.snapchat.com/@<username>, so one string split gives you the handle.
import json, os, requests
rules = {
"name_h1": {"selector": "h1 span", "output": "text"}, # creator profile
"name_h4": {"selector": "h4 span", "output": "text"}, # basic card
"subtitle": {"selector": "h5 + div", "output": "text"},
"snapcode": {"selector": '[data-testid="snapCodeImage"]', "output": "@src"},
"profile_image": {"selector": 'meta[property="og:image"]', "output": "@content"},
"canonical": {"selector": 'link[rel="canonical"]', "output": "@href"},
}
r = requests.get(
"https://app.scrapingbee.com/api/v1/",
headers={"Authorization": f"Bearer {os.environ['SCRAPINGBEE_API_KEY']}"},
params={
"url": "https://www.snapchat.com/add/teamsnapchat",
"extract_rules": json.dumps(rules),
"mode": "auto",
},
)
print(r.json())Live response on the basic card:
{
"name_h1": "",
"name_h4": "Team Snapchat",
"subtitle": "Add me on Snapchat!",
"snapcode": "https://app.snapchat.com/web/deeplink/snapcode?username=teamsnapchat&type=SVG&bitmoji=enable",
"profile_image": "https://www.snapchat.com/web-capture/www.snapchat.com/@teamsnapchat/preview/square.jpeg?xp_id=1",
"canonical": "https://www.snapchat.com/@teamsnapchat"
}One more asymmetry worth handling. The Snapcode data-testid is present on basic cards and empty on creator profiles, where the same rule returned "". You do not need a second request for it, because the endpoint is parameterised by username, so it can be rebuilt from the canonical handle:
https://app.snapchat.com/web/deeplink/snapcode?username=<handle>&type=SVG&bitmoji=enable
That constructed string matches the live selector value exactly, verified on both handles.
That request cost 1 credit. Not 5, not 25. Snapchat serves its profile pages as delivered HTML, so mode=auto settled on the plain rung and never opened a browser. Of the five ScrapingBee targets tested this week, this is the cheapest. Do not reach for render_js here out of habit.
The /add/ path answers with a 308 redirect to /@username, which is a canonical redirect rather than a block. Both forms work as input.
The numbers people actually want are not in the DOM text at all. They are in an application/ld+json block. On a page with a following count, the DOM holds only the i18n template placeholder, literally {subscriberCount} suscriptores, so a text selector returns the template rather than the number.
Note that extract_rules cannot read script tag contents. A rule of {"jsonld": {"selector": "script[type=\"application/ld+json\"]", "output": "@text"}} returns null. This was tested. Fetch the page and parse the block yourself.
Live ProfilePage block for a creator account:
{
"@type": "ProfilePage",
"mainEntity": {
"@type": "Person",
"name": "MrBeast",
"alternateName": "mrbeast",
"url": "https://www.snapchat.com/@mrbeast",
"image": "https://cf-st.sc-cdn.net/aps/bolt/...",
"interactionStatistic": [
{
"@type": "InteractionCounter",
"interactionType": { "@type": "FollowAction" },
"userInteractionCount": 1463200
}
],
"identifier": { "@type": "PropertyValue", "propertyID": "Username", "value": "mrbeast" }
},
"inLanguage": "es-ES",
"isFamilyFriendly": true,
"dateCreated": "2019-05-16T14:46:37.345Z",
"dateModified": "2026-08-13T13:01:39.438Z"
}Four fields here exist nowhere else on the page:
interactionStatistic[0].userInteractionCountis the follower count. 1,463,200 on that capture.dateCreatedis when the public profile was created. Useful for account age checks.dateModifiedis the last content update, which is how you tell an active creator from a dormant handle without fetching their posts.isFamilyFriendlyis Snapchat's own content flag.
A second block, an ItemList, holds public Spotlight and highlight videos as VideoObject entries with url, name and thumbnailUrl. Five items on the creator page tested. The same block came back as an empty array on teamsnapchat, so treat an empty ItemList as normal rather than as an error.
This one costs people real time. The first capture of the creator page came back in Spanish:
MrBeast (@mrbeast) | Historias de Snapchat, Spotlight y Lentes
A later capture of the same URL, same parameters, came back in Romanian: og:title read MrBeast pe Snapchat and og:description read MrBeast este pe Snapchat!. Two requests, two languages, no parameter changed between them.
Snapchat localises by proxy exit IP. Without a country parameter, whichever region the request left from decides the language of every title, subtitle and label you parse. The structured data tells you it happened, via "inLanguage": "es-ES".
The fix is to pin the country:
curl -G "https://app.scrapingbee.com/api/v1/" \
-H "Authorization: Bearer $SCRAPINGBEE_API_KEY" \
--data-urlencode "url=https://www.snapchat.com/add/mrbeast" \
-d premium_proxy=true -d country_code=usWhich returned the English MrBeast on Snapchat, and an identical userInteractionCount of 1463200. So the follower number is locale independent and so are the canonical URL, the username and the preview image. Only og:title, og:description, the page title and the visible labels move.
Geotargeting requires the premium tier, and that call measured 25 credits against 1 credit for the unpinned version. That is a twenty five times cost increase to stabilise strings you may not be reading, so pin the country only when you actually parse them.
pip install snapchat-scraper-api
npm install snapchat-scraper-apifrom snapchat_scraper_api import SnapchatScraper
bee = SnapchatScraper("YOUR_API_KEY")
bee.profile("teamsnapchat") # basic card, 1 credit
bee.profile("mrbeast") # creator profile, 1 credit, same call
bee.stats("mrbeast") # followers, created, modified, locale
bee.spotlight("mrbeast") # public video list, empty list is normal
bee.exists("dailymail") # False, no public pageprofile() coalesces the two heading layouts, derives the username from the canonical URL and rebuilds the Snapcode when the selector is empty, so the same call returns a populated record for both page types.
snapchat.com/add/dailymail has no public profile, and it answers in two different ways. One capture returned HTTP 200 with 5,973 bytes and a bare Snapchat title and no ProfilePage block. A later capture returned HTTP 404, passed straight through from Snapchat, because 404 is one of the few statuses ScrapingBee forwards rather than rewriting.
So a missing handle can be either a 200 with an empty shell or a real 404. Handle both, and decide on the presence of the ProfilePage block rather than on the status code, or you will record empty rows as successful scrapes and let a 404 crash the run.
Measured, not quoted:
| Configuration | Credits |
|---|---|
mode=auto on a public profile, settled on plain HTML |
1 |
premium_proxy=true with country_code |
25 |
| Validation error | 0 |
mode=auto charges only for the rung that succeeded and charges nothing if every rung fails. It is incompatible with render_js, premium_proxy and stealth_proxy, and sending both returns HTTP 400. Plan tiers are on the pricing page.
Public profile pages only. Private accounts, friend lists, Snap Map location data, direct messages, story views and anything requiring a signed in session are out of scope, and scraping under login credentials is prohibited by ScrapingBee's terms of service. Snapchat's Terms of Service and Community Guidelines govern use of the data.
Reference: extraction rules, screenshot capture for rendering verification, and markdown output if you are feeding profile pages to a model.
How do I get a Snapchat follower count?
From interactionStatistic[0].userInteractionCount in the application/ld+json block. It is not in the visible text, where you will find only the i18n placeholder.
Why did my selector stop working? Almost certainly because it was a hashed CSS module class. Move up to rung 3 or 4.
Can I scrape Snapchat Spotlight videos?
Public ones, yes, from the ItemList block, as VideoObject entries with URL, name and thumbnail. Expect an empty array on accounts with no public video.
Does this need JavaScript rendering? No, and turning it on wastes credits. Plain HTML carried every field at 1 credit per request.
Why does my scraper work on some Snapchat profiles and not others?
Because there are two layouts. The display name is in an h1 on creator profiles and an h4 on basic user cards, and the Snapcode attribute only exists on the latter. Request both headings and coalesce.
MIT. See LICENSE.