Instagram API Scraper: Engagement Rate Per Follower avatar

Instagram API Scraper: Engagement Rate Per Follower

Pricing

Pay per usage

Go to Apify Store
Instagram API Scraper: Engagement Rate Per Follower

Instagram API Scraper: Engagement Rate Per Follower

Pricing

Pay per usage

Rating

0.0

(0)

Developer

API Empire

API Empire

Maintained by Community

Actor stats

0

Bookmarked

7

Total users

5

Monthly active users

a day ago

Last modified

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, likesPerFollower and commentsPerFollower
  • 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 detailsfollowersCount, 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, avgComments and — always — postsAnalysed and ratedPostsAnalysed
  • 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 TypeKey FieldsJSON Field Names
Posts (section: "posts")Caption, hashtags, likes, comments, timestamp, media URLs, carousel children, plus every follower-denominated metricshortCode, 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 sizetype ("AccountSummary"), isSummary, ownerUsername, avgEngagementRate, avgLikes, avgComments, maxEngagementRate, minEngagementRate, postsAnalysed, ratedPostsAnalysed, engagementTier
Comments (section: "comments")Comment text, author, likes, reply count, parent postid, text, ownerUsername, ownerProfilePicUrl, timestamp, likesCount, repliesCount, owner, postUrl, postShortCode
Profile details (section: "profiles")Full public profile recordusername, fullName, biography, externalUrl, followersCount, followsCount, postsCount, isVerified, isPrivate, isBusinessAccount, profilePicUrl, profilePicUrlHD, category
Mentions (section: "mentions")One row per @handle found in a captiontype ("mention"), mentionedUsername, ownerUsername, postShortCode, postUrl, timestamp, inputUrl
Search results (section: "search")Users, hashtags or places matching a querytype, 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 billedtype ("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 — likesCount and commentsCount. 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: followersCount for the post's owner, recovered once per account by ProfileEnricher and 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 AccountSummary row.
  • 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, not 0.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:

SituationfollowersCountengagementRateengagementTierlikesPerFollower / commentsPerFollowerengagementTotal
Follower count recoveredintegernumber (4 dp)tier labelnumbers (4 dp)number
Profile lookup failed or timed outnullnullnullnullnumber
Account reports zero followers0nullnullnullnumber

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-null engagementRate values in the slice, rounded to 4 dp. If no post in the slice has a usable rate, this is null, not 0.
  • avgLikes / avgComments — mean of the non-null likesCount / commentsCount values, rounded to 4 dp; null on an empty sample.
  • maxEngagementRate / minEngagementRate — the best and worst rate in the slice, or null when there are none.
  • postsAnalysed — the size of the slice. Always published.
  • ratedPostsAnalysed — how many of those posts had a usable follower count. When this is 0, every average above is null by 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:

engagementRateengagementTier
nullnull
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.

FeatureInstagram Graph API (Meta's Instagram Platform)Instagram API Scraper: Engagement Rate Per Follower
Accounts you can queryInstagram Professional accounts you own or that have granted your app access, via a linked Facebook PageAny public Instagram profile, post, reel or hashtag
Credentials requiredFacebook app, OAuth access token, permission scopesNone — .actor/actor.json has no credential field
Approval processApp review before production permissionsNone; run the Actor from the Apify Console immediately
Follower-denominated engagement rateNot returned; you compute it from insights endpoints yourselfReturned per post as engagementRate, plus a per-account avgEngagementRate
Entity coverage in one callSeparate endpoints per resourcePosts, reels, comments, profiles, mentions and search in one Actor via resultsType
Output shapeMeta's JSON, versioned with the Graph APIStable Apify dataset rows with fixed key names, exportable as JSON/CSV/Excel/XML
Competitor dataLimited to the Business Discovery surface for professional accountsAny 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.

  1. 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.
  2. Fill in accountsToAnalyse — bare usernames like humansofny, 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 in directUrls) or a non-empty search.
  3. Choose resultsTypeposts for the engagement-rate workflow, or comments, details, mentions, reels, stories — and set resultsLimit per URL. Add onlyPostsNewerThan to bound the window and minEngagementRate to screen by threshold.
  4. Tune the rate settings: leave computeEngagementRate and fetchFollowerCounts on, set postsToAverage to your rollup window, and raise profileFetchTimeout if you run through a slow proxy.
  5. 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.txt records that plain requests gets HTTP 429 on /api/v1/users/web_profile_info/ while a Chrome fingerprint returns 200, and src/engagement.py ships three fingerprints (chrome124, chrome120, chrome110) tried in order. Header spoofing alone no longer clears the check.
  • Search moved behind auth. src/scraper.py logs, on a non-200 from /api/v1/web/search/topsearch/: "Instagram has been requiring auth for /web/search/topsearch since 2025". Expect the search input to return few or zero rows on a logged-out run.
  • Rotating GraphQL document IDs. The shortcode query's doc_id is 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.py documents that an Apify proxy group literally named DATACENTER returned HTTP 407 on every request; the Actor warms BUYPROXIES94952 instead.

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.

ParameterRequiredTypeDescriptionExample Value
accountsToAnalyseNoarray 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/"]
computeEngagementRateNobooleanDocumented as adding engagementRate using (likes + comments) ÷ followers × 100. Default: true. See the caveat below the table.true
postsToAverageNointeger — min 1, max 500How 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
minEngagementRateNointeger — min 0, max 100Keep 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
fetchFollowerCountsNobooleanLook 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
profileFetchTimeoutNointeger seconds — min 5, max 120Per-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
directUrlsNoarray 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/"]
resultsTypeNostring, select — enum posts, comments, details, mentions, reels, storiesWhat to pull per URL. An unrecognized value logs a warning and falls back to posts. Default: "posts"."posts"
resultsLimitNointeger — min 1, max 100000Max 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
onlyPostsNewerThanNostring, datepicker, absoluteOrRelativeAbsolute 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"
addParentDataNobooleanAdds a parentData object (name, type, url) naming the source profile or hashtag to each post/reel/mention row. Default: false.false
searchNostring, textfieldRun an Instagram search alongside or instead of the URLs. Empty disables search. Default: ""."streetphotography"
searchTypeNostring, select — enum user, hashtag, placeWhat the query targets. An unrecognized value falls back to hashtag. Default: "hashtag"."hashtag"
searchLimitNointeger — min 1, max 250Number of search results. Schema default: 1. If absent or below 1 the engine substitutes 20; above 250 is clamped to 250.20
proxyConfigurationNoobject, proxy editorDefault 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: false does not switch the maths off on its own. The follower lookup is gated on computeEngagementRate OR fetchFollowerCounts, and the decoration step then runs unconditionally on posts/reels rows. With computeEngagementRate: false and fetchFollowerCounts at its default true, the profile is still fetched and engagementRate still written. Set both to false to genuinely skip it — then every follower-denominated field comes back null.
  • resultsLimit and searchLimit differ between Console and API. The Console prefills the schema defaults (10 and 1). A raw API call that omits the keys gets the engine fallbacks — 30 and 20 — three times and twenty times the billable rows. Always send the keys explicitly from code.
  • resultsType: "stories" runs the reels scraper. reels and stories share 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 under resultsType: "posts" goes down the post-detail path and lands in the details section — and only posts and reels sections are decorated. Those rows carry no engagementRate, no followersCount, and produce no AccountSummary. 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 type
section != "accounting" # by section tag
errorReason == 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?

FeatureInstagram API Scraper: Engagement Rate Per FollowerGeneric Instagram follower/profile scraper
Entity coverage in one ActorPosts, reels, comments, profile details, mentions, search results and per-account rollups, selected by resultsTypeTypically one entity — follower lists, or follower counts, or posts
Engagement rateComputed per post as engagementRate and per account as avgEngagementRate, formula published aboveNot returned; you join follower counts to post counts yourself
Missing-data handlingnull never 0, plus an uncharged Accounting row naming the account and reasonUsually a silent 0 or a dropped row
Sample-size disclosurepostsAnalysed and ratedPostsAnalysed shipped beside every averageAverages published without the denominator
CredentialsNone — no login, no cookies, no sessionid field in the schemaVaries; several require a session cookie
Anti-block handlingAutomatic ladder: direct → datacenter → residential, sticky on residential, plus three rotating Chrome TLS fingerprints on the profile endpointVaries; 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 ApifyClient
client = 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 row
print(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 sectionposts 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.

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.