How to Scrape Instagram (Python Guide)
- Instagram blocks logged-out scraping. In my June 2026 test the profile page returned 200 but served a login wall: no follower count, no posts, empty
og:tags. The old?__a=1JSON endpoint is dead and theweb_profile_infoAPI returned 429 on the first hit. - Plain requests + BeautifulSoup gets you nothing useful past the page title. There is no public HTML payload to parse.
- Scraping public Instagram data is contested law: Meta's Terms prohibit it, but a US court declined to block Bright Data from scraping public Meta data in January 2024. Personal data triggers GDPR regardless.
- Realistic options: the official Instagram Graph API for your own or consenting business accounts, or a scraper API like ChocoData that rotates residential IPs and renders the page for public profiles at volume.
Instagram is one of the most requested scraping targets and one of the most hostile. I spent June 2026 testing what a plain Python scraper can pull from a logged-out session, and the answer is close to nothing. The profile page returns a 200 status code, which looks encouraging, then hands you a login wall with the follower count, post grid, and bio stripped out. This guide shows the real probe output, explains how Instagram blocks anonymous requests, walks the legal picture with primary sources, and covers the two approaches that actually return data: the official Graph API and a scraper API.
Can you legally scrape Instagram?
Scraping public Instagram data sits in contested legal territory, and Meta’s own Terms prohibit it outright. Three layers matter, and they are independent.
First, Meta’s contract. The Instagram Terms of Use state that you cannot “collect information in an automated way without our prior permission.” Meta’s broader Terms of Service carry the same prohibition. Scraping Instagram, logged in or out, breaches that contract, and Meta has sued scrapers over it.
Second, US computer-crime and contract law, which is unsettled for public data. In Meta Platforms v. Bright Data, the US District Court for the Northern District of California denied Meta’s motion and declined to hold Bright Data liable for scraping publicly available Facebook and Instagram data, in a January 2024 order. That ruling tracks the reasoning in hiQ v. LinkedIn: scraping data that is public and ungated is hard to frame as unauthorized access. The ruling does not bless scraping; it declined to stop one company on the specific facts.
Third, data-protection law, which applies regardless of the two above. Instagram profiles are personal data. The moment you store information on EU or UK residents, GDPR applies, and “it was public” is not a lawful basis on its own. I cover the general framing in my is web scraping legal guide.
The practical posture: scraping public, non-personal Instagram data (aggregate hashtag trends, public business-account metrics) carries the lowest risk. Collecting personal profiles of private individuals carries the highest. Logging in a bot account breaches the Terms most directly and risks the account.
What data is available on Instagram?
Public Instagram pages display rich data in a browser, but almost none of it reaches a logged-out scraper as parseable markup. Here is what each surface theoretically exposes versus what a plain logged-out request actually returns:
| Data point | Visible in browser | Logged-out scraper gets it? |
|---|---|---|
| Username, full name | Yes | Page title only (“Instagram”) |
| Follower / following count | Yes | No, login wall |
| Post count | Yes | No, login wall |
| Bio, external link | Yes | No, login wall |
| Post images and captions | Yes | No, login wall |
| Likes / comment counts | Yes | No, login wall |
| Hashtag post feeds | Yes | No, login wall |
| Stories, Reels | Yes | No, login wall |
The pattern is blunt: Instagram renders everything client-side behind authentication, so the initial HTML a scraper receives is a shell. The next section shows the proof I pulled in June 2026.
How does Instagram block scrapers?
Instagram blocks logged-out scrapers by serving a login wall instead of data, killing its old JSON endpoints, and rate-limiting its internal API on the first request. I tested all three surfaces in June 2026 with plain Python. Here is the probe I ran:
import urllib.request, gzip, re
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0 Safari/537.36")
def fetch(url, extra=None):
headers = {"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"}
if extra:
headers.update(extra)
req = urllib.request.Request(url, headers=headers)
try:
r = urllib.request.urlopen(req, timeout=25)
raw = r.read()
try:
raw = gzip.decompress(raw)
except Exception:
pass
return r.status, raw.decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, ""
# 1. Public profile page
status, body = fetch("https://www.instagram.com/instagram/")
title = re.search(r"<title>(.*?)</title>", body, re.S)
print("PROFILE:", status, "| len", len(body))
print("TITLE:", title.group(1).strip() if title else "NONE")
print("og:description present:", '<meta property="og:description"' in body)
print("login form present:", "loginForm" in body.lower())
# 2. Legacy public JSON endpoint
status2, body2 = fetch("https://www.instagram.com/instagram/?__a=1&__d=dis")
print("\n?__a=1 JSON:", status2, "| len", len(body2))
# 3. Internal web_profile_info API
status3, body3 = fetch(
"https://i.instagram.com/api/v1/users/web_profile_info/?username=instagram",
extra={"x-ig-app-id": "936619743392459"},
)
print("web_profile_info API:", status3, "| len", len(body3))
The real output, run June 2026:
PROFILE: 200 | len 593006
TITLE: Instagram
og:description present: False
login form present: True
?__a=1 JSON: 201 | len 0
web_profile_info API: 429 | len 0
Read that carefully, because the 200 is a trap. The profile page request succeeded, returned a 593 KB body, and gave back a login form with the title “Instagram”, not “Instagram (@instagram)”. The Open Graph description that used to carry the follower count is gone. The legacy ?__a=1 endpoint, which scrapers leaned on for years, now returns a 201 with an empty body: dead. The internal web_profile_info API, the last endpoint that returned profile JSON, answered 429 Too Many Requests on my very first hit from a clean IP. Even instagram.com/robots.txt served me an HTML login document rather than a robots file when I fetched it logged-out.
So there is no public HTML payload to parse and no surviving anonymous JSON endpoint. This is why I am not pasting a “tested” stamp on a working DIY parser: the honest result is that plain requests against Instagram return a wall. The blocking stack is layered: server-side rendering gating, retired public endpoints, aggressive rate limits, and behind those, IP and device fingerprinting that flags datacenter traffic fast.
How do you scrape Instagram with Python? (Method 1: DIY)
The DIY path for public data means rendering the page in a real browser through Playwright, because requests alone only ever sees the login wall. Even then you are fighting the blocks above, and an unauthenticated browser still gets gated on most profile data. The code below launches a headless Chromium, loads a profile, and reads whatever the rendered DOM exposes:
from playwright.sync_api import sync_playwright
def scrape_profile(username: str):
url = f"https://www.instagram.com/{username}/"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(
user_agent=("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0 Safari/537.36")
)
page.goto(url, wait_until="networkidle", timeout=30000)
# Instagram often shows a login modal over the content.
# The meta description, when present, holds the public counts.
meta = page.query_selector('meta[property="og:description"]')
print("og:description:", meta.get_attribute("content") if meta else "BLOCKED / login wall")
title = page.title()
print("title:", title)
browser.close()
scrape_profile("instagram")
I am not pasting output for this one, because I will not fabricate it and a headless run from a flagged datacenter IP routinely hits the same login modal my requests probe did. Playwright is the right tool when a target is JS-heavy, and my Selenium and Playwright scraping guide covers the rendering mechanics. But for Instagram specifically, headless automation from a single datacenter IP is detected quickly: you get the login modal, a checkpoint, or a rate limit. If you log in to bypass the modal, you breach Meta’s Terms head-on and risk the account. That is the wall every DIY Instagram scraper hits. For the parsing fundamentals under all of this, see my BeautifulSoup guide and the broader Python scraping guide.
What about the official Instagram Graph API?
The Instagram Graph API is the only sanctioned way to pull Instagram data, and it works for accounts you own or that authorize you. Meta provides it for Instagram Business and Creator accounts connected to a Facebook Page. Your app requests access through Facebook Login, the account owner grants it, and you receive an access token that returns media, comments, mentions, and insights through documented endpoints.
| Feature | Graph API | DIY scraping | Scraper API |
|---|---|---|---|
| Sanctioned by Meta | Yes | No | No |
| Arbitrary public profiles | No | Blocked in practice | Yes |
| Your own / consenting accounts | Yes | n/a | n/a |
| Audience insights, reach | Yes | No | No |
| Stable (no selector breakage) | Yes | No | Partial |
| Account ban risk | None | High (if logged in) | None |
The catch is in the second row: the Graph API will not return data for a random competitor’s profile by username. It only serves accounts that have authorized your app. For your own brand analytics, audience demographics, or a client who connects their account, it is the correct and durable choice. Start at Meta’s Instagram Platform documentation. For public competitive data on accounts that will never authorize you, the Graph API is a dead end, which is where a scraper API comes in.
How do you scrape public Instagram data at scale? (Method 2: scraper API)
Route the request through a scraper API so it renders the page from a real, residential session and rotates IPs, then you parse the structured response it returns. This is the practical answer for public profile data at volume, because it solves the exact failures my probe exposed: the login wall, the datacenter-IP detection, and the rate limiting. The scraper API maintains the proxy pool and the rendering pipeline so you get usable data instead of a 429.
ChocoData is one such service, with a universal endpoint plus a set of dedicated endpoints. Its request shape is a single GET carrying your target URL, your API key, and a flag to render JavaScript. Here is the pattern for sending an Instagram profile URL through the universal endpoint:
import requests
API_KEY = "YOUR_CHOCODATA_KEY"
target = "https://www.instagram.com/instagram/"
r = requests.get(
"https://api.chocodata.com/api/v1/universal",
params={
"api_key": API_KEY,
"url": target,
"render_js": "true",
},
timeout=60,
)
print("STATUS:", r.status_code)
# r.text holds the rendered HTML from a residential IP;
# parse the profile fields with BeautifulSoup as you would
# any rendered page.
I did not run this snippet, because it needs a paid key, so treat the field names as illustrative and confirm the exact parameters against ChocoData’s own docs before you rely on them. The principle holds across any scraper API: you hand it the URL, it returns the page from a rotating residential IP with JavaScript rendered, and the detection problem becomes the vendor’s problem instead of yours. ChocoData advertises residential proxy rotation and a universal endpoint alongside 453 dedicated endpoints, which are the features that matter against Instagram’s rendering gate and IP fingerprinting.
The honest framing: a scraper API removes the technical block, not the legal one. Routing through residential proxies does not grant you permission under Meta’s Terms, and it does not exempt you from GDPR when you collect personal profiles. Use it for public, non-personal data, apply the same restraint you would to any scraping that needs to avoid getting blocked, and keep personal data on private individuals out of your dataset.
Should you build your own Instagram scraper or use an API?
Use the Graph API for accounts you control, a scraper API for public data at volume, and skip the pure DIY route for Instagram. Here is the decision in one table:
| Your goal | Best approach | Why |
|---|---|---|
| Your own brand’s insights | Graph API | Sanctioned, stable, richer data |
| A client’s account (they authorize) | Graph API | Same, with their consent |
| Public competitor profiles, low volume | Scraper API | DIY hits the login wall |
| Public profiles or hashtags at scale | Scraper API | Handles IP rotation and rendering |
| One-off, public, hobby | DIY (Playwright) | Free, but expect blocks |
The pattern I would follow: if the data is about your own or a consenting account, the Graph API is the clear winner, full stop. If you need public data that no one will authorize, a scraper API is the only route that returns it reliably, and you carry the Terms and privacy risk yourself. Pure requests plus BeautifulSoup, the stack that works for so many sites, returns a login wall on Instagram, as my June 2026 probe showed.
For the fundamentals under all of this, including headers, rendering, and parsing, see the web scraping guide. If you want to run a browser-based scraper inside a larger crawl, the Scrapy guide shows how to structure it, and the Python scraping guide covers the requests-and-parsing stack end to end.
FAQ
Not for arbitrary public profiles. Meta offers the Instagram Graph API, but it only returns data for Instagram Business or Creator accounts that you own or that have explicitly authorized your app through Facebook Login. There is no official, free endpoint that hands you any public profile's followers or posts by username. The legacy public endpoints (?__a=1 and the unofficial web_profile_info call) are either dead or rate-limited to uselessness, which I confirmed by testing them in June 2026.
Barely. A logged-out GET of a profile URL returns a 200, but the body is a login wall, not data. In my test the page title was just 'Instagram', the Open Graph and meta-description tags were empty, and the HTML contained a login form instead of the follower count or post grid. Everything substantive is loaded behind authentication. To get real public data you either authenticate (which deepens the Terms-of-Service breach) or route through a scraper API that renders the page from a real session.
It can. Meta's Terms of Use prohibit collecting data using automated means without prior permission, and logging in a bot account to scrape is the fastest way to trigger an action limit, a checkpoint, or a permanent ban. The account-ban risk applies specifically when you scrape while authenticated. Scraping public pages logged-out avoids the account risk but still breaches the Terms and, for residents' personal data, implicates GDPR.
For your own brand or for business accounts that authorize you, use the official Instagram Graph API: it is sanctioned, stable, and returns insights plain scraping cannot. For public competitive or research data at volume, a scraper API is the practical route, but it removes the technical block, not the legal one. Whichever you choose, avoid collecting personal data on private individuals, and read Meta's Terms and your local privacy law before you store anything.