Instagram API Scraper: Engagement Rate Per Follower
Pricing
Pay per usage
Instagram API Scraper: Engagement Rate Per Follower
Pricing
Pay per usage
Rating
0.0
(0)
Developer
API Empire
Maintained by CommunityActor stats
0
Bookmarked
7
Total users
5
Monthly active users
a day ago
Last modified
Categories
Share
Instagram Scraper — Posts, Reels, Profiles and Engagement Rate JSON
This Instagram scraper extracts posts, reels, comments, profile details, hashtag mentions and search results from public Instagram accounts, then divides each post's interactions by that account's real follower count to produce a follower-denominated engagementRate. Every row is typed, normalized JSON — no HTML, no CSS selectors, no parsing step. It also emits one AccountSummary rollup per account with the sample size printed beside every average. By the end of this page you will know exactly which keys land in your dataset and what each one costs.
What is Instagram API Scraper: Engagement Rate Per Follower?
Instagram API Scraper: Engagement Rate Per Follower is an Apify Actor that scrapes public Instagram data over plain HTTP — no login, no cookies, no sessionid, no headless browser — and enriches every post and reel with the metric raw scrapers leave out: engagement rate per follower. A base Instagram scraper hands you likesCount and commentsCount, which are meaningless without a denominator. A 4,200-like post is extraordinary on a 20,000-follower micro-creator and near-dead on a 20-million-follower celebrity account. This Actor recovers the follower count and does the division.
There is no credential field anywhere in the input schema: .actor/actor.json declares fifteen properties and not one accepts an Instagram username, password, session cookie or token. The Actor authenticates nothing — it reads public surfaces the way a logged-out browser does.
Key capabilities, one per entity type the Actor genuinely returns:
- Scrape Instagram posts from any public profile or hashtag, decorated with
engagementRate,engagementTier,likesPerFollowerandcommentsPerFollower - Scrape Instagram reels from a public profile feed, decorated with the same follower-denominated metrics
- Scrape Instagram comments from a post or reel, with commenter identity and like counts
- Scrape Instagram profile details —
followersCount,followsCount,postsCount,biography,externalUrl, verified and private flags - Scrape hashtag mentions and Instagram search results for users, hashtags and places
- Get a per-account rollup row with
avgEngagementRate,avgLikes,avgCommentsand — always —postsAnalysedandratedPostsAnalysed - Export as JSON, CSV, Excel, XML or HTML from the Apify dataset; no proxy management and no parsing on your side
📊 What data does this Instagram scraper collect?
The Actor writes eight structurally distinct row types into one dataset: posts, reels, comments, profile details, mentions, search results, account summaries and uncharged accounting rows. Every row carries a section key so you can split or filter the export, and .actor/actor.json ships eight pre-built dataset views (📈 Engagement rate, 🪪 Account profiles, 📷 Posts overview, 💬 Comments view, 🪪 Profile details, 🎬 Reels view, 🔖 Mentions view, 🔎 Search results) that select the relevant columns for each.
| Data Type | Key Fields | JSON Field Names |
|---|---|---|
Posts (section: "posts") | Caption, hashtags, likes, comments, timestamp, media URLs, carousel children, plus every follower-denominated metric | shortCode, caption, hashtags, likesCount, commentsCount, timestamp, displayUrl, childPosts, engagementRate, engagementTier, engagementTotal, likesPerFollower, commentsPerFollower, followersCount |
Reels (section: "reels") | Same key set as posts, with type forced to "Video" | type, shortCode, likesCount, commentsCount, displayUrl, engagementRate, engagementTier, ownerUsername |
Account summary (section: "summary") | Per-account rollup with its own sample size | type ("AccountSummary"), isSummary, ownerUsername, avgEngagementRate, avgLikes, avgComments, maxEngagementRate, minEngagementRate, postsAnalysed, ratedPostsAnalysed, engagementTier |
Comments (section: "comments") | Comment text, author, likes, reply count, parent post | id, text, ownerUsername, ownerProfilePicUrl, timestamp, likesCount, repliesCount, owner, postUrl, postShortCode |
Profile details (section: "profiles") | Full public profile record | username, fullName, biography, externalUrl, followersCount, followsCount, postsCount, isVerified, isPrivate, isBusinessAccount, profilePicUrl, profilePicUrlHD, category |
Mentions (section: "mentions") | One row per @handle found in a caption | type ("mention"), mentionedUsername, ownerUsername, postShortCode, postUrl, timestamp, inputUrl |
Search results (section: "search") | Users, hashtags or places matching a query | type, id, username, full_name, follower_count, name, media_count, lat, lng, url |
Accounting rows (section: "accounting") | Diagnostics for accounts whose follower count could not be recovered — never billed | type ("Accounting"), ownerUsername, url, errorReason, message |
That is seven billable entity types plus one free diagnostic type from a single run — the difference between a breadth scraper and a single-purpose follower counter.
Need more Instagram data?
API Empire publishes several focused Instagram Actors that pair well with this one. Instagram Audience Quality Checker looks at the audience rather than the posts, and Instagram Profile Bio Contact Extractor pulls contact details out of bios at scale. For content research, Instagram Hashtag Content Planner and Instagram Reel Hashtag Mention Extractor cover discovery, while Instagram Comment Moderation Scraper and Instagram Mention Sentiment Analyzer handle the conversation around a post. Rank accounts here first, then hand the winners to whichever fits.
📈 How is the engagement rate calculated?
The formula lives in one function, engagement_rate() in src/engagement.py, and it is the industry-standard follower-denominated definition:
engagementRate = (likesCount + commentsCount) / followersCount * 100
Read the details off the implementation, because the details are where engagement-rate tools usually disagree:
- Which interactions are summed: exactly two —
likesCountandcommentsCount. Nothing else. Saves, shares, story replies and video views are not available on the public surfaces this Actor reads, so they are not in the numerator and are not silently estimated. - What it is divided by:
followersCountfor the post's owner, recovered once per account byProfileEnricherand cached for the rest of the run. It is not divided by reach, impressions or the account's own average. - How many posts: none — this is a per-post figure. Each post row gets its own rate computed from its own likes and comments. Averaging happens separately, in the
AccountSummaryrow. - Percentage or fraction: a percentage, rounded to 4 decimal places. 4,200 likes plus 130 comments on a 120,000-follower account yields
engagementRate: 3.6083, not0.036083.
Two derived percentages ship alongside it, on the same denominator and rounding: likesPerFollower = likesCount / followersCount * 100, commentsPerFollower = commentsCount / followersCount * 100. engagementTotal is the plain undivided sum likesCount + commentsCount.
The divide-by-zero path
engagement_rate() guards the denominator before dividing: if followersCount is None, 0 or negative it returns None. It never returns 0, never estimates, and never substitutes a platform average. What lands in the row:
| Situation | followersCount | engagementRate | engagementTier | likesPerFollower / commentsPerFollower | engagementTotal |
|---|---|---|---|---|---|
| Follower count recovered | integer | number (4 dp) | tier label | numbers (4 dp) | number |
| Profile lookup failed or timed out | null | null | null | null | number |
| Account reports zero followers | 0 | null | null | null | number |
A null rate is visible in a spreadsheet and in a WHERE engagementRate IS NULL query. A fabricated 0 is not — it looks like a real, terrible rate and quietly poisons every average downstream. Every account that fails enrichment also produces an uncharged Accounting row with errorReason: "profile_enrichment_unavailable", so a null column always has a paper trail.
The zero-posts path in the rollup
build_account_summary() slices posts[:postsToAverage] and averages only entries that have values:
avgEngagementRate— mean of the non-nullengagementRatevalues in the slice, rounded to 4 dp. If no post in the slice has a usable rate, this isnull, not0.avgLikes/avgComments— mean of the non-nulllikesCount/commentsCountvalues, rounded to 4 dp;nullon an empty sample.maxEngagementRate/minEngagementRate— the best and worst rate in the slice, ornullwhen there are none.postsAnalysed— the size of the slice. Always published.ratedPostsAnalysed— how many of those posts had a usable follower count. When this is0, every average above isnullby construction.
An account only gets a rollup row if at least one post or reel was scraped for it, so postsAnalysed is never 0, but ratedPostsAnalysed frequently is on blocked runs. Read them as a pair: avgEngagementRate: 4.11 over ratedPostsAnalysed: 12 is a claim; over ratedPostsAnalysed: 1 it is an anecdote.
Engagement tier bands
engagementTier is a labelling convention applied to the computed number, not a measurement of your corpus. The bands are hard-coded in src/engagement.py:
engagementRate | engagementTier |
|---|---|
null | null |
| below 0.5 | 🔴 Low |
| 0.5 up to (not including) 1.0 | 🟠 Below average |
| 1.0 up to (not including) 3.0 | 🟢 Average |
| 3.0 up to (not including) 6.0 | 🔵 High |
| 6.0 and above | 🟣 Very high |
The same bands are applied to avgEngagementRate to set engagementTier on the AccountSummary row.
How does this Instagram scraper differ from the official Instagram Graph API?
Meta's Instagram Platform API is an owner-side API: it returns data about accounts you or your clients control, after you have built a Facebook app, obtained an access token, and passed app review for the permissions you need. This Actor is an observer-side scraper: it returns the public data any logged-out visitor can see, for any public account, with no app, no token and no review.
| Feature | Instagram Graph API (Meta's Instagram Platform) | Instagram API Scraper: Engagement Rate Per Follower |
|---|---|---|
| Accounts you can query | Instagram Professional accounts you own or that have granted your app access, via a linked Facebook Page | Any public Instagram profile, post, reel or hashtag |
| Credentials required | Facebook app, OAuth access token, permission scopes | None — .actor/actor.json has no credential field |
| Approval process | App review before production permissions | None; run the Actor from the Apify Console immediately |
| Follower-denominated engagement rate | Not returned; you compute it from insights endpoints yourself | Returned per post as engagementRate, plus a per-account avgEngagementRate |
| Entity coverage in one call | Separate endpoints per resource | Posts, reels, comments, profiles, mentions and search in one Actor via resultsType |
| Output shape | Meta's JSON, versioned with the Graph API | Stable Apify dataset rows with fixed key names, exportable as JSON/CSV/Excel/XML |
| Competitor data | Limited to the Business Discovery surface for professional accounts | Any public account, including ones you have no relationship with |
Meta's Instagram Platform documentation is the authority on what the Graph API grants and how to apply. No rate-limit or quota figures are quoted here — they vary by app tier and are only accurate in Meta's docs.
Use the Graph API when you own the accounts, need private insights such as reach and saves, and can carry the app-review overhead. Use this Actor when you need public, cross-account, competitor-inclusive data and a comparable engagement rate today.
Why do developers and teams scrape Instagram?
Instagram engagement data drives four very different workflows, and the fields that matter differ sharply between them.
For AI engineers and agent builders
Every row is typed JSON with stable keys, so it drops into a retrieval pipeline without a parsing layer. A typical agent tool calls the Actor with resultsType: "posts" and resultsLimit: 24, embeds caption plus hashtags into a vector store, and stores engagementRate, engagementTier and timestamp as metadata filters. The agent then answers "show me this brand's above-average posts from last quarter" as a filtered similarity search rather than a scrape-and-hope loop. Because engagementRate is null rather than 0 when unknown, filters never silently include unmeasured rows.
For marketers and influencer teams
Follower count is the vanity metric; engagement rate per follower is the one that predicts campaign performance. Feed a creator shortlist into accountsToAnalyse, set postsToAverage: 12, and read the AccountSummary rows: avgEngagementRate beside ratedPostsAnalysed gives you both the number and how much to trust it. Set minEngagementRate: 3 and the run keeps only posts clearing 3%. biography and externalUrl come back on the same rows, so outreach lists build themselves.
For researchers and analysts
The Actor touches only publicly visible Instagram surfaces — no login, no cookies, no private endpoints — which keeps the collection scope defensible in a methods section. A typical study samples N public accounts, fixes postsToAverage so every account contributes the same window, and reports postsAnalysed and ratedPostsAnalysed as the realized per-account sample size. Because missing denominators produce null instead of 0, non-response stays measurable rather than being absorbed into the mean, and the Accounting rows document which accounts dropped out and why.
For developers building data products
Schedule the Actor from the Apify Console, point a webhook at your ingest endpoint, and you have a normalized engagement feed without maintaining TLS fingerprints, proxy ladders or GraphQL document IDs. The dataset shape is stable across runs, so INSERT INTO posts (short_code, owner_username, engagement_rate, ...) keeps working. Filter type != "Accounting" on ingest to drop diagnostics and use section to route rows into per-entity tables. The Apify API serves the dataset as JSON or CSV, so any language that speaks HTTP can consume it.
How to scrape Instagram engagement rates (step by step)
This Actor runs on the Apify platform only — from the Apify Console UI or through the Apify API. There is no separate signup and no service-specific API key.
- Open the Actor on its Apify Store listing and click Try for free, or open it in the Apify Console if you already have it saved.
- Fill in
accountsToAnalyse— bare usernames likehumansofny, full profile URLs, or/p/<shortcode>/and/reel/<shortcode>/links. Nothing is schema-required, but the run stops immediately unless you supply at least one target here (or indirectUrls) or a non-emptysearch. - Choose
resultsType—postsfor the engagement-rate workflow, orcomments,details,mentions,reels,stories— and setresultsLimitper URL. AddonlyPostsNewerThanto bound the window andminEngagementRateto screen by threshold. - Tune the rate settings: leave
computeEngagementRateandfetchFollowerCountson, setpostsToAverageto your rollup window, and raiseprofileFetchTimeoutif you run through a slow proxy. - Start the run, watch the live log (it prints each post with its likes and comments), then download the results as JSON, CSV, Excel, XML or HTML — or read them straight from the Apify API.
What to do when Instagram changes its structure
Instagram rotates endpoints, document IDs and anti-bot checks continuously. This Actor is maintained against those changes, and the dataset schema is the contract: key names, types and the null-not-zero rule stay put, so database columns, dashboards and agent tools keep working across updates. A run that suddenly returns empty sections or a flood of Accounting rows is the signal to report.
What changed in Instagram scraping recently?
The most consequential shift is that Instagram now gates its public JSON endpoints on TLS client fingerprint, not just on headers. Everything below is traceable to this Actor's own source, not to hearsay.
- TLS fingerprint gating.
requirements.txtrecords that plainrequestsgets HTTP 429 on/api/v1/users/web_profile_info/while a Chrome fingerprint returns 200, andsrc/engagement.pyships three fingerprints (chrome124,chrome120,chrome110) tried in order. Header spoofing alone no longer clears the check. - Search moved behind auth.
src/scraper.pylogs, on a non-200 from/api/v1/web/search/topsearch/: "Instagram has been requiring auth for /web/search/topsearch since 2025". Expect thesearchinput to return few or zero rows on a logged-out run. - Rotating GraphQL document IDs. The shortcode query's
doc_idis a constant Meta rotates; the Actor keeps a three-step fallback (GraphQL →/api/v1/media/<shortcode>/info/→ embedded page JSON) because any one path can be turned off. - Proxy-group churn.
src/main.pydocuments that an Apify proxy group literally namedDATACENTERreturned HTTP 407 on every request; the Actor warmsBUYPROXIES94952instead.
For DIY scrapers, breakage is routine rather than exceptional. For users of this Actor no action is required: fingerprints, fallbacks and the proxy ladder are maintained inside it, and the output keys do not move when the transport does.
⬇️ Input
Every parameter below is read directly from .actor/actor.json. None is marked required — but the run exits immediately if you supply neither a target URL (in accountsToAnalyse or directUrls) nor a non-empty search.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
accountsToAnalyse | No | array of strings (stringList editor) | Profiles, posts or reels to measure; bare usernames work. Prefilled ["https://www.instagram.com/humansofny/"]. Merged with directUrls, de-duplicated, order preserved. No schema default. | ["humansofny", "https://www.instagram.com/nasa/"] |
computeEngagementRate | No | boolean | Documented as adding engagementRate using (likes + comments) ÷ followers × 100. Default: true. See the caveat below the table. | true |
postsToAverage | No | integer — min 1, max 500 | How many scraped posts per account feed the AccountSummary rollup (avgEngagementRate, avgLikes, avgComments), with postsAnalysed always published beside them. Missing or 0 falls back to 12. Default: 12. | 12 |
minEngagementRate | No | integer — min 0, max 100 | Keep only posts whose engagementRate is greater than or equal to this percentage; posts with a null rate are dropped too whenever this is above 0. Applies to posts/reels rows only. Default: 0 (keep everything). | 3 |
fetchFollowerCounts | No | boolean | Look each account up once per run for followersCount, followsCount, postsCount, biography, externalUrl (unwrapped from l.instagram.com), isVerifiedAccount, isPrivateAccount. One request per account, cached for the run. Default: true. | true |
profileFetchTimeout | No | integer seconds — min 5, max 120 | Per-profile lookup timeout. On timeout the posts still return with engagementRate: null plus an uncharged accounting row. Values below 5, or missing, are clamped up. Default: 25. | 25 |
directUrls | No | array of strings (stringList editor) | Legacy key from the original Instagram API Scraper. Merged with accountsToAnalyse rather than overriding it, so saved inputs keep working. Also accepts {"url": "..."} objects. No schema default. | ["https://www.instagram.com/natgeo/"] |
resultsType | No | string, select — enum posts, comments, details, mentions, reels, stories | What to pull per URL. An unrecognized value logs a warning and falls back to posts. Default: "posts". | "posts" |
resultsLimit | No | integer — min 1, max 100000 | Max items per URL. Schema default: 10. If the key is absent from the run input or below 1, the engine substitutes 30 — see the pitfall note. | 24 |
onlyPostsNewerThan | No | string, datepicker, absoluteOrRelative | Absolute YYYY-MM-DD or a relative window. The schema pattern accepts day/week/month/year; the parser also accepts hour. Months count as 30 days, years as 365. UTC. No default. | "90 days" |
addParentData | No | boolean | Adds a parentData object (name, type, url) naming the source profile or hashtag to each post/reel/mention row. Default: false. | false |
search | No | string, textfield | Run an Instagram search alongside or instead of the URLs. Empty disables search. Default: "". | "streetphotography" |
searchType | No | string, select — enum user, hashtag, place | What the query targets. An unrecognized value falls back to hashtag. Default: "hashtag". | "hashtag" |
searchLimit | No | integer — min 1, max 250 | Number of search results. Schema default: 1. If absent or below 1 the engine substitutes 20; above 250 is clamped to 250. | 20 |
proxyConfiguration | No | object, proxy editor | Default and prefill: {"useApifyProxy": false} — direct to Instagram. Enabling Apify Proxy forces a starting tier: a RESIDENTIAL group starts and stays residential, any other group starts on datacenter. Either way the Actor warms both pools at startup and escalates automatically when blocked. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
Behaviour worth knowing before you run
computeEngagementRate: falsedoes not switch the maths off on its own. The follower lookup is gated oncomputeEngagementRateORfetchFollowerCounts, and the decoration step then runs unconditionally onposts/reelsrows. WithcomputeEngagementRate: falseandfetchFollowerCountsat its defaulttrue, the profile is still fetched andengagementRatestill written. Set both tofalseto genuinely skip it — then every follower-denominated field comes backnull.resultsLimitandsearchLimitdiffer between Console and API. The Console prefills the schema defaults (10and1). A raw API call that omits the keys gets the engine fallbacks —30and20— three times and twenty times the billable rows. Always send the keys explicitly from code.resultsType: "stories"runs the reels scraper.reelsandstoriesshare one code path; output is counted and emitted as reels. There is no separate story fetch.- Single post and reel URLs do not get an engagement rate. A
/p/<shortcode>/or/reel/<shortcode>/input underresultsType: "posts"goes down the post-detail path and lands in thedetailssection — and onlypostsandreelssections are decorated. Those rows carry noengagementRate, nofollowersCount, and produce noAccountSummary. To measure engagement rate, pass profile URLs or bare usernames. This is the most common input mistake with this Actor.
Example JSON input
{"accountsToAnalyse": ["humansofny","https://www.instagram.com/nasa/","natgeo"],"computeEngagementRate": true,"postsToAverage": 12,"minEngagementRate": 0,"fetchFollowerCounts": true,"profileFetchTimeout": 25,"directUrls": [],"resultsType": "posts","resultsLimit": 24,"onlyPostsNewerThan": "90 days","addParentData": false,"search": "","searchType": "hashtag","searchLimit": 1,"proxyConfiguration": {"useApifyProxy": false}}
⬆️ Output
Results are pushed live to the run's default dataset as each item is produced, so you can start reading rows before the run finishes. Every row is typed, normalized JSON with stable key names and a section tag, and Apify exports the dataset as JSON, JSONL, CSV, Excel, XML, RSS or HTML. Eight dataset views are pre-declared, so the Console shows the right columns per row type with no configuration.
Scraped post (with engagement rate)
This is the full shape of a section: "posts" row from a profile input, with all follower-denominated fields present.
{"id": "3412998765432109876","type": "Sidecar","shortCode": "C8xTqLpMv2Z","caption": "Twelve years on the same corner. #humansofny #streetphotography @nikonusa","hashtags": ["humansofny", "streetphotography"],"mentions": ["nikonusa"],"url": "https://www.instagram.com/p/C8xTqLpMv2Z/","commentsCount": 130,"firstComment": "This one hit hard.","latestComments": [ "…see the comment shape below — same keys, nested here…" ],"dimensionsHeight": 1350,"dimensionsWidth": 1080,"displayUrl": "https://scontent.cdninstagram.com/v/t51.../main.jpg","images": ["https://scontent.cdninstagram.com/v/t51.../main.jpg","https://scontent.cdninstagram.com/v/t51.../child1.jpg"],"alt": "A man in a grey coat standing beside a newsstand","likesCount": 4200,"timestamp": "2026-06-18T13:05:44.000Z","childPosts": [ "…one object per carousel slide — key set listed below…" ],"ownerFullName": "Humans of New York","ownerUsername": "humansofny","ownerId": "1234567890","isCommentsDisabled": false,"inputUrl": "humansofny","section": "posts","followersCount": 120000,"engagementRate": 3.6083,"engagementTier": "🔵 High","likesPerFollower": 3.5,"commentsPerFollower": 0.1083,"engagementTotal": 4330,"followsCount": 312,"postsCount": 7841,"biography": "New York City, one story at a time.","externalUrl": "https://www.humansofnewyork.com","isVerifiedAccount": true,"isPrivateAccount": false,"profileSource": "web_profile_info"}
Each childPosts entry carries id, type ("Image"/"Video"), shortCode, url, displayUrl, dimensionsHeight, dimensionsWidth, alt, ownerId, caption, hashtags, mentions, images, latestComments, childPosts, commentsCount, firstComment, likesCount, timestamp — the last four are always empty, 0 or null on children. latestComments entries use the comment shape shown below. parentData appears only when addParentData is true: {"name": "humansofny", "type": "profile", "url": "https://www.instagram.com/humansofny/"}.
Scraped reel
A reel row carries the exact same key set as the post row above — including all thirteen enrichment keys — but lands in section: "reels" with type forced to "Video" and childPosts always empty. The distinguishing values:
{"id": "3401122334455667788","type": "Video","shortCode": "C8kLmNoPqRs","caption": "Behind the shot. #bts","hashtags": ["bts"],"url": "https://www.instagram.com/p/C8kLmNoPqRs/","likesCount": 18740,"commentsCount": 402,"timestamp": "2026-06-11T17:40:12.000Z","displayUrl": "https://scontent.cdninstagram.com/v/t51.../reelcover.jpg","dimensionsHeight": 1920,"dimensionsWidth": 1080,"childPosts": [],"ownerUsername": "humansofny","section": "reels","followersCount": 120000,"engagementRate": 15.9517,"engagementTier": "🟣 Very high","likesPerFollower": 15.6167,"commentsPerFollower": 0.335,"engagementTotal": 19142,"profileSource": "web_profile_info"}
Account summary rollup
One row per account, emitted after every post for that account has been seen.
{"type": "AccountSummary","isSummary": true,"ownerUsername": "humansofny","url": "https://www.instagram.com/humansofny/","followersCount": 120000,"followsCount": 312,"postsCount": 7841,"biography": "New York City, one story at a time.","externalUrl": "https://www.humansofnewyork.com","isVerifiedAccount": true,"isPrivateAccount": false,"profileSource": "web_profile_info","postsAnalysed": 12,"ratedPostsAnalysed": 12,"avgEngagementRate": 4.1172,"avgLikes": 4712.5,"avgComments": 228.1667,"maxEngagementRate": 15.9517,"minEngagementRate": 1.2044,"engagementTier": "🔵 High","section": "summary"}
Note the key collision worth planning for: minEngagementRate is both an input parameter (the filter threshold) and an output key on this row (the lowest rate in the sample). They are unrelated values.
Scraped comment
{"id": "17998234512340987","text": "This one hit hard.","ownerUsername": "marta.reads","ownerProfilePicUrl": "https://scontent.cdninstagram.com/v/t51.../marta.jpg","timestamp": "2026-06-18T14:22:07.000Z","repliesCount": 2,"replies": null,"likesCount": 41,"owner": {"username": "marta.reads","id": "1873456210","full_name": "Marta R.","profile_pic_url": "https://scontent.cdninstagram.com/v/t51.../marta.jpg","profile_pic_id": "3390012345678901234","is_verified": false,"is_private": false,"is_mentionable": true,"latest_reel_media": 0},"postUrl": "https://www.instagram.com/p/C8xTqLpMv2Z/","postShortCode": "C8xTqLpMv2Z","section": "comments"}
replies is always null — the Actor returns the reply count in repliesCount, not the reply bodies.
Scraped profile details
Produced by resultsType: "details" on a profile URL. This row is not decorated with engagement fields.
{"id": "1234567890","username": "humansofny","fullName": "Humans of New York","biography": "New York City, one story at a time.","externalUrl": "https://l.instagram.com/?u=https%3A%2F%2Fwww.humansofnewyork.com","followersCount": 120000,"followsCount": 312,"postsCount": 7841,"isVerified": true,"isPrivate": false,"isBusinessAccount": false,"profilePicUrl": "https://scontent.cdninstagram.com/v/t51.../pp.jpg","profilePicUrlHD": "https://scontent.cdninstagram.com/v/t51.../pp_hd.jpg","category": "Photographer","inputUrl": "humansofny","section": "profiles"}
externalUrl on this row is returned exactly as Instagram serves it, which may be a wrapped l.instagram.com redirect. The externalUrl written by the enrichment path onto post, reel and summary rows is unwrapped to the destination.
Scraped mention
{"type": "mention","mentionedUsername": "nikonusa","postShortCode": "C8xTqLpMv2Z","postUrl": "https://www.instagram.com/p/C8xTqLpMv2Z/","ownerUsername": "humansofny","timestamp": "2026-06-18T13:05:44.000Z","inputUrl": "humansofny","section": "mentions"}
Search result
Shape varies by searchType. A hashtag result:
{"type": "hashtag","id": "17841562893027714","name": "streetphotography","media_count": 128400000,"search_result_subtitle": "128M posts","url": "https://www.instagram.com/explore/tags/streetphotography/","section": "search"}
A user result returns type, id, username, full_name, is_verified, is_private, profile_pic_url, follower_count; a place result returns type, id, name, subtitle, lat, lng, url.
Accounting row (never charged)
{"type": "Accounting","ownerUsername": "someblockedaccount","url": "https://www.instagram.com/someblockedaccount/","errorReason": "profile_enrichment_unavailable","message": "Follower count could not be recovered for this account, so engagementRate is null (not zero) on its posts.","section": "accounting"}
💸 What gets charged, and what does not
This Actor is pay-per-event with a single event: row_result. One row_result is charged for each row pushed to the dataset — post, reel, comment, profile detail, mention, search result and AccountSummary rows all count.
Accounting rows are pushed uncharged. Every account whose follower count could not be recovered produces one diagnostic row explaining the null, written without a charged event. That is the only free row type — if a row is in your dataset and it is not an Accounting row, it was billed. Rows removed by minEngagementRate are never pushed at all, so they are never charged; note they are still counted in the AccountSummary averages, because the rollup accumulates posts before the filter runs.
To exclude the free diagnostic rows downstream, filter on any of these:
type != "Accounting" # by row typesection != "accounting" # by section tagerrorReason == null # by presence of the diagnostic field
Cost-control notes from the engine's actual behaviour: resultsType: "mentions" on a profile emits one charged row per @handle in a caption, so a post with five mentions bills five rows; a hashtag input under resultsType: "posts" triggers one profile lookup and one charged AccountSummary row for each distinct post owner; and an API call that omits resultsLimit gets 30 items per URL instead of the schema's 10.
How does this Actor compare to other Instagram scrapers?
| Feature | Instagram API Scraper: Engagement Rate Per Follower | Generic Instagram follower/profile scraper |
|---|---|---|
| Entity coverage in one Actor | Posts, reels, comments, profile details, mentions, search results and per-account rollups, selected by resultsType | Typically one entity — follower lists, or follower counts, or posts |
| Engagement rate | Computed per post as engagementRate and per account as avgEngagementRate, formula published above | Not returned; you join follower counts to post counts yourself |
| Missing-data handling | null never 0, plus an uncharged Accounting row naming the account and reason | Usually a silent 0 or a dropped row |
| Sample-size disclosure | postsAnalysed and ratedPostsAnalysed shipped beside every average | Averages published without the denominator |
| Credentials | None — no login, no cookies, no sessionid field in the schema | Varies; several require a session cookie |
| Anti-block handling | Automatic ladder: direct → datacenter → residential, sticky on residential, plus three rotating Chrome TLS fingerprints on the profile endpoint | Varies; often requires you to configure a proxy yourself |
For context, quoted only as the competing listings state it and reviewed on the Apify Store 2026-07-25 — none of it measured here: apify/instagram-followers-count-scraper returns follower and following counts with no engagement-rate output and directs users to a separate Actor per Instagram entity type. scraping_solutions/instagram-scraper-followers-following-no-cookies advertises follower/following list export with continuation tokens and states free-tier API runs are capped at 1,000 results per run. kaitoeasyapi/premium-x-follower-scraper-following-data targets X/Twitter rather than Instagram and advertises "$0.1 per 1,000 profiles", "50-70 profiles per second" and a "99.9% uptime guarantee" — no equivalent figures are claimed for this Actor.
If you're building an AI agent or RAG pipeline, the output-format row is the decision-maker — parsing HTML inside an agent loop is a reliability failure mode, not a feature. The second row matters almost as much: an agent that has to compute engagement rate itself needs a second data source for follower counts and a place to put the join.
How many results can you scrape?
There is no hard cap inside the Actor beyond resultsLimit, which accepts up to 100000 per URL, and searchLimit, which is clamped to a hard ceiling of 250 in the engine regardless of what you send. The real ceiling is Instagram's own pagination.
Pagination as implemented: profile posts walk the user feed endpoint with max_id cursors, twelve items per page, stopping when resultsLimit is reached, when onlyPostsNewerThan is crossed, when the feed reports no more pages, or at a hard cap of 200 pages. Reels paginate the same way through the clips endpoint with paging_info.max_id. Three consecutive failed pages abort that profile. Between pages the Actor sleeps a randomized 2.0–3.0 seconds, and 1.5–3.0 seconds between input URLs.
Two platform-side limits, both documented in this Actor's own input schema and source: Instagram's profile pagination stops near 2,400 items per profile, so a resultsLimit far above that returns no more; and comments mode returns only the preview comments attached to the post payload — there is no comment-pagination loop, so resultsLimit caps what Instagram already sent, not what exists.
No benchmark timings are published here because none have been measured for this Actor.
Integrate this Instagram scraper and automate your workflow
This Actor works with any language or tool that can send an HTTP request to the Apify API. It runs on the Apify platform only — from the Apify Console or via the Apify API — using your Apify API token. There is no separate service key.
REST API integration
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_API_TOKEN>")run = client.actor("<YOUR_USERNAME>/instagram-api-scraper-engagement-rate-per-follower").call(run_input={"accountsToAnalyse": ["humansofny", "natgeo"],"resultsType": "posts","resultsLimit": 24,"postsToAverage": 12,"minEngagementRate": 0,"computeEngagementRate": True,"fetchFollowerCounts": True,})for item in client.dataset(run["defaultDatasetId"]).iterate_items():if item.get("type") == "Accounting":continue # free diagnostic rowprint(item.get("ownerUsername"), item.get("engagementRate"), item.get("engagementTier"))
Works in Python, Node.js, Go, Ruby and cURL — the Apify API is plain REST, and the dataset can also be fetched directly as JSON or CSV from /v2/datasets/<datasetId>/items.
Automation platforms (n8n, Make, LangChain)
n8n — use the Apify node with the Run Actor operation, point it at this Actor's ID, paste the input JSON, and chain Get Dataset Items. An IF node on type != "Accounting" separates billable data from diagnostics before your database node.
Make — the Apify app provides Run an Actor and Watch Actor Runs modules. Run on a schedule, then iterate the dataset and route rows by section — posts to a sheet, summary to a scorecard, accounting to an alert channel.
LangChain — wrap the Actor call in a tool function returning the dataset items directly. Rows are already typed JSON with stable keys, so tool output enters the model context with no parsing step; engagementRate, engagementTier and postsAnalysed make good structured-output fields for a creator-vetting chain.
The Apify platform's own integrations — webhooks, scheduling, Zapier, Slack, Google Sheets, Google Drive, Airbyte — also work with this Actor's dataset without extra code.
Is it legal to scrape Instagram?
Scraping publicly accessible Instagram data is generally lawful in the jurisdictions where Apify Actors commonly run, but what you do with the data afterwards is what regulators care about. This Actor returns only publicly available data: it performs no login, holds no credentials, and reads nothing that a logged-out visitor cannot see. Private accounts are not accessed.
The output contains personal data — usernames, full names, biographies, profile pictures and comment text belonging to identifiable people. That brings it within the GDPR in the EU/UK and the CCPA in California. You need a lawful basis before you store, enrich or profile that data, you need to be able to honour access and erasure requests, and legitimate-interest assessments should be documented before bulk collection, not after.
Consult legal counsel for commercial use cases involving bulk personal data.
❓ Frequently asked questions
Does this Instagram scraper work without an Instagram account?
Yes. No Instagram account, password, cookie or sessionid is required or accepted — the input schema contains no credential field at all. The Actor reads public endpoints over plain HTTP with rotating browser TLS fingerprints. You only need an Apify account to run the Actor itself.
How is the engagement rate actually calculated?
(likesCount + commentsCount) / followersCount * 100, per post, rounded to 4 decimal places, expressed as a percentage. Only likes and comments are in the numerator. The denominator is the post owner's follower count, fetched once per account and cached for the run. If the follower count is missing, zero or negative the result is null, never 0. Full detail — including the tier bands and the rollup rules — is in the engagement-rate section above.
How often is the scraped data updated?
Every run fetches live from Instagram. Nothing is cached between runs. Within a single run, one profile lookup per username is cached and reused for all of that account's posts, so a 24-post run costs one profile request, not 24. Schedule the Actor in the Apify Console for recurring snapshots.
What happens when a post is deleted, or an account is private or blocked?
Deleted posts simply do not appear in the feed response — the Actor returns fewer items rather than erroring. If a profile lookup fails or times out, its posts are still returned with followersCount, engagementRate, engagementTier, likesPerFollower and commentsPerFollower all null, plus an uncharged Accounting row carrying errorReason: "profile_enrichment_unavailable". Three consecutive failed pages abort that profile and the run moves to the next input. A run with no usable targets ends with zero items rather than a crash.
Can I scrape private Instagram accounts or login-gated content?
No. Only publicly accessible content is returned. Private accounts, follower and following lists, direct messages, and anything behind Instagram's login wall are out of scope — the Actor never authenticates, so it cannot see them. Instagram search is also affected: the Actor's own log notes that /api/v1/web/search/topsearch/ has required auth since 2025, so the search input may return few or no rows on a logged-out run.
Where do I run this Actor, and what do I need?
On the Apify platform — from the Apify Console UI or through the Apify API with your Apify API token. There is no separate signup and no service-specific key. Pricing is pay-per-event on row_result; the current rate is shown on the Actor's Apify Store listing.
Does this work for AI agent workflows and LLM pipelines?
Yes. The Actor is callable as an HTTP endpoint by any agent framework — LangChain, LlamaIndex, a custom tool-use loop, or an n8n/Make automation. Every response is typed, normalized JSON with stable field names: no HTML, no selectors, no parsing step before passing rows to an LLM, and nested structures (latestComments, childPosts, owner, parentData) are real JSON objects rather than stringified blobs. Filter type != "Accounting" for a clean stream, use section to route entity types, index caption and hashtags into a vector store with engagementRate and timestamp as metadata filters, and treat null on engagementRate as genuinely unknown rather than as a low score.
How does the Actor handle Instagram's anti-bot system?
With three countermeasures, all implemented in the source. TLS impersonation — the enrichment client tries chrome124, chrome120 and chrome110 fingerprints in order. A proxy escalation ladder — the run starts direct, escalates to an Apify datacenter pool on a blocking status (401, 403, 407, 429, 451, 503), then to residential, and once on residential it stays there rotating among six pre-warmed endpoints. Endpoint fallbacks — profile data falls back from the JSON API to HTML extraction; post detail falls back from GraphQL to the media info endpoint to embedded page JSON. Randomized delays sit between pages and URLs, and 429/503 responses back off progressively.
How does it compare to other Instagram scrapers on the Apify Store?
The observable difference is entity breadth plus the follower denominator. Neither apify/instagram-followers-count-scraper nor scraping_solutions/instagram-scraper-followers-following-no-cookies returns a per-post engagementRate, an engagementTier, or a rollup row carrying its own postsAnalysed — see the comparison section above for what their listings do advertise, as reviewed on 2026-07-25. This Actor returns seven billable entity types in one run and publishes the engagement formula rather than a black-box score.
Can I use this without managing proxies?
Yes. The default proxyConfiguration is {"useApifyProxy": false} — the Actor talks to Instagram directly and escalates to Apify's datacenter and then residential pools only when Instagram pushes back. Both fallback pools are warmed at startup whether or not you enable proxies, so no configuration is needed. To force a starting tier, enable Apify Proxy and pick a group: a RESIDENTIAL group starts and stays residential; any other group starts on datacenter.
What happens when Instagram changes its structure or blocks the scraper?
The Actor is maintained, and the dataset schema is the contract — field names, types and the null-not-zero rule stay stable on your end, so ingest jobs and dashboards keep working across transport changes. Multiple fallback paths already exist for the endpoints most likely to move. No numeric turnaround time is promised here.
Your feedback
Found a bug, hit an empty section, or missing a field you need? We want to know — concrete reports are what keep this Actor working. Open an issue on the Issues tab of the Actor in the Apify Console, and include the run ID, the exact input JSON, and the field or section that behaved unexpectedly. Feature requests for additional output keys or resultsType modes are welcome on the same channel.