Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Snapchat Scraper API

snapchat-scraper-api

checks pypi python npm node license

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.

Rung 0: hashed classes. Never use these.

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.

Rung 1: positional selectors, and the two layout problem

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.

Rung 2: test identifiers

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.

Rung 3: OpenGraph and canonical. The durable rung.

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.

Rung 4: structured data. The richest rung.

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].userInteractionCount is the follower count. 1,463,200 on that capture.
  • dateCreated is when the public profile was created. Useful for account age checks.
  • dateModified is the last content update, which is how you tell an active creator from a dormant handle without fetching their posts.
  • isFamilyFriendly is 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.

The locale trap

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=us

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

Ready made packages

pip install snapchat-scraper-api
npm install snapchat-scraper-api
from 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 page

profile() 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.

Handles that have no page

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.

Credit cost

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.

Scope

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.

FAQ

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.

License

MIT. See LICENSE.