How to Scrape Instagram Data with an API (2026)
DIY browser scraping vs Meta Graph API vs a data API for public Instagram profiles, posts, and reels — tradeoffs and runnable code.
There are three ways to pull public Instagram data into a product: drive your own headless browser, use Meta's Instagram Graph API, or call an Instagram data API that hands back normalized JSON. Meta's Graph API only covers accounts you own or that grant your app access. A DIY Instagram scraper is a maintenance treadmill of GraphQL churn and login walls. For most teams, a data API is the pragmatic way to get public profiles, posts, and reels as clean JSON. This guide compares all three — what each one really costs, where it breaks, and which one fits your project — with runnable code for every path, including the ones we don't sell.
The shortest path is a single request:
curl -sS \
-H "x-api-key: $SOCIALFETCH_API_KEY" \
"https://api.socialfetch.dev/v1/instagram/profiles/instagram"You'll need an API key and curl or the TypeScript SDK. New to the API? Start with the Quickstart.
The three ways to get Instagram data
Every approach is some version of these three. The right one depends on volume, who's using the data, and how much maintenance you want to own.
| Approach | Best when | The real cost |
|---|---|---|
| DIY (Puppeteer / Playwright) | A one-off research pull, internal tooling, or you specifically want to own the pipeline | Residential proxies, session babysitting, GraphQL doc-id churn — and re-fixing it every time Instagram ships a web change |
| Instagram Graph API | You already have app review and your need maps to accounts you manage | Business verification, review, and a hard ownership wall — arbitrary public creators are out of scope by design |
| Data API (Social Fetch) | You need profiles, posts, reels, search, or transcripts in product code now, ideally across more than one platform | Metered per completed lookup; you depend on a provider to keep the scrapers working |
For most production apps that need public creator data on demand, the data API is the fastest route to reliable JSON — one REST surface across Instagram, TikTok, YouTube, and X, with the same auth header and the same data + meta envelope. DIY is genuinely worth it for a weekend research scrape or to learn the internals. Graph is the right call when you're already inside Meta's partner programs for owned accounts.
The rest of this guide covers each path, starting with the one most teams try first.
Why DIY scraping turns into a maintenance project
A plain fetch() to instagram.com gets blocked almost immediately — Instagram's bot defenses inspect TLS fingerprints, cookies, and request patterns that a stock HTTP client does not match. So a working scraper isn't a script; it's a small system you build and then maintain indefinitely. Here's roughly what the browser-driven version looks like:
from playwright.async_api import async_playwright
import asyncio, random
async def scrape_instagram_profile(username: str):
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=False,
proxy={
"server": "http://residential-proxy.example:9000",
"username": "user",
"password": "pass",
},
)
context = await browser.new_context(
user_agent=pick_real_user_agent(),
locale="en-US",
timezone_id="America/New_York",
)
page = await context.new_page()
await page.goto("https://www.instagram.com/", wait_until="networkidle")
await asyncio.sleep(random.uniform(2.0, 5.0))
await page.goto(
f"https://www.instagram.com/{username}/",
wait_until="networkidle",
)
# Profile fields usually live in embedded GraphQL / shared-data JSON,
# not in stable DOM selectors. You still parse opaque blobs after this.
html = await page.content()
await browser.close()
return htmlThat snippet only gets you raw HTML. The numbers you care about usually sit in embedded GraphQL / shared-data JSON blobs you still have to locate and parse. And around it you're standing up:
- A residential or mobile proxy pool. Datacenter IPs are flagged within a few hits, so you need real ones with sticky, region-matched rotation.
- Session and login-wall handling. Public pages often redirect into login or challenge flows mid-cron. You patch around it, then Instagram changes the wall and your overnight job returns empty grids.
- GraphQL document churn. Query hashes and
doc_idvalues move without a changelog. A parser that worked Tuesday returns empty arrays Friday — and you hear it from a customer, not a test. - Behavioral pacing. Fixed intervals read as a bot, so you add randomized delays and cap concurrency — slowing the whole thing down.
The build is the easy part. The maintenance is worse: Instagram quietly renames internal fields and reshapes media objects, so a scraper that looked solid in staging drifts in production. For a one-off pull that's a fine weekend. For anything customer-facing, you've signed up for a maintenance subscription paid in your own evenings. For the broader build-vs-buy math, see Social Fetch vs DIY scraping.
Why Meta's Graph API won't cover it
Meta's Instagram developer products are the right answer when your app lives inside Meta's ecosystem — publishing on a Business or Creator account you manage, reading insights for assets that granted access, or messaging with approved permissions. None of these are a general "look up this public handle" tool:
- Instagram Graph API exposes profiles, media, insights, and comments for accounts you own or that authorize your app. App review and Business verification sit in front of most production scopes.
- Basic Display / consumer login paths (where still relevant to your stack) are for user-authorized personal data in your app — not bulk competitor enrichment.
- Marketing / ads APIs cover campaigns and ad accounts, not arbitrary public creator lookups.
So if you need web-parity public data in a generic backend, Graph leaves a gap by design — the ownership wall is intentional. Reach for Graph when your use case maps to owned assets; reach for a data API when it doesn't. Terms and product names change often — check Meta for Developers before building against any of them.
Using the Social Fetch API
Social Fetch exposes Instagram under documented /v1/instagram/** routes — profiles, posts, reels, stories/highlights, search, comments, transcripts, and more. One header authenticates every call, and responses share the same data + meta envelope across platforms, so your Instagram code looks identical to your TikTok or YouTube code.
Hover underlined tokens for details.
Get a profile
The same lookup in cURL, the TypeScript SDK, Python, and more — switch tabs to match your stack:
const response = await fetch(
"https://api.socialfetch.dev/v1/instagram/profiles/instagram",
{
headers: {
"x-api-key": process.env.SOCIALFETCH_API_KEY,
},
}
);
const body = await response.json();
console.log(response.status, body);Most Instagram profile lookups are 1 credit when the lookup completes (including private / not_found outcomes that still return HTTP 200). Trust meta.creditsCharged on every response.
List a profile's posts
const response = await fetch(
"https://api.socialfetch.dev/v1/instagram/profiles/instagram/posts",
{
headers: {
"x-api-key": process.env.SOCIALFETCH_API_KEY,
},
}
);
const body = await response.json();
console.log(response.status, body);Full parameters live in the reference: Get Instagram profile and Profile posts. Reels, highlights, search, and transcripts sit next to them in the API reference. Profile post and reel list routes are also 1 credit per completed page.
Paginate through everything
Post and reel list endpoints return one page plus a cursor. To pull a full history, pass the cursor back verbatim until it's gone — no decoding, no arithmetic:
import { SocialFetchClient } from "@socialfetch/sdk";
const client = new SocialFetchClient({
apiKey: process.env.SOCIALFETCH_API_KEY!,
});
const allPosts = [];
let cursor: string | undefined;
do {
const result = await client.instagram.getProfilePosts({
handle: "natgeo",
cursor,
});
if (!result.ok) {
console.error(result.error.code, result.error.requestId);
break;
}
allPosts.push(...result.value.data.posts);
cursor = result.value.data.page.nextCursor ?? undefined;
} while (cursor);Stop when data.page.nextCursor is null. Don't loop on a total count — upstream totals are estimates and often disagree with what's actually paginatable. The cursor is the source of truth. Each page is a separate completed lookup for billing.
Reading the response
A successful response wraps the payload alongside billing metadata, so you always know what a call cost:
{
"data": {
"lookupStatus": "found",
"profile": {
"platform": "instagram",
"handle": "natgeo",
"displayName": "National Geographic",
"verified": true,
"privateAccount": false
},
"metrics": {
"followers": 280000000,
"following": 140,
"posts": 28000
}
},
"meta": {
"requestId": "req_01example",
"creditsCharged": 1,
"version": "v1"
}
}The one gotcha: an HTTP 200 does not guarantee lookupStatus: "found". A private or non-existent account can return 200 with a status field telling you what actually happened — handle it in application logic, not by trusting the status code. Every response also carries meta.requestId, so if a number ever looks wrong, support can trace the exact lookup. See Errors and Credits.
Post detail with downloadMedia=true is the common surcharge case: 11 credits on success (1 base + 10 for hosted media download). Leave downloadMedia off unless you need the files.
What you can build
- Influencer vetting — pull profile metrics and recent posts before outreach, and reconcile CRM handles against live numbers so you're not paying creators on stale stats.
- Competitive content monitoring — snapshot competitor profiles, posts, and reels on a schedule without running a scraper per brand.
- Brand-safety and topic pipelines — pair captions with post/reel transcripts when on-screen text is thin.
For the same identity pattern across Instagram, TikTok, and YouTube, see Cross-platform creator profiles.
FAQ
Is scraping Instagram legal?
It depends on your jurisdiction, what you collect, and how you use it. Collecting public, business-level metrics for B2B analytics is a common pattern, and courts (e.g. hiQ v. LinkedIn) have generally treated public-web data as fair game in several jurisdictions. You remain responsible for Instagram's terms, applicable privacy law, and your own contracts. This is a technical guide, not legal advice — talk to counsel if you're collecting at scale.
Can't I just use the Instagram Graph API?
Not for arbitrary public creator lookups. Graph covers accounts you manage or that grant your app access. For web-parity public data in a generic backend, a data API is the usual fit. See Why Meta's Graph API won't cover it.
How fresh is the data?
Each request fetches live from Instagram at call time — there's no cache returning an hour-old snapshot. If a number looks off, meta.requestId lets support trace that specific lookup.
What happens if a profile is private or doesn't exist?
Check data.lookupStatus. Don't treat a 200 alone as "data is present" — a private or missing account returns 200 with a status field. Details in Errors.
How are credits charged?
Credits charge when a lookup completes, including 200 outcomes like private or not_found. Most Instagram profile, post-list, reel-list, search, highlight, and transcript routes are 1 credit per completed request. Post detail with downloadMedia=true is 11 credits on success. Validation failures and upstream lookup_failed / temporary unavailability do not bill the same way — see Credits. Always reconcile against meta.creditsCharged.
How does this compare to other providers?
See the side-by-side comparisons: vs Apify, vs Bright Data, vs EnsembleData, the full compare hub, and Best Instagram data APIs in 2026.
Next steps: Quickstart · Instagram platform hub · Instagram API reference · Pricing