Instagram Profile Scraper - $0.70 per 1,000 Profiles
Pricing
from $0.70 / 1,000 profile scrapeds
Instagram Profile Scraper - $0.70 per 1,000 Profiles
Scrape Instagram profiles from handles or profile URLs. One row per profile: exact follower, following and post counts. You also get the verified flag, business category, e-mail, phone and bio links. A private account hides the posts, not the bio or the counts. $0.70/1,000 profiles.
Pricing
from $0.70 / 1,000 profile scrapeds
Rating
0.0
(0)
Developer
Dami's Studio
Maintained by CommunityActor stats
0
Bookmarked
47
Total users
29
Monthly active users
5 days ago
Last modified
Categories
Share
Instagram Profile Scraper
Making an Instagram account private hides the posts and nothing else. The bio, the picture and all three counts stay public, and this reads both kinds. A list of handles goes in and one row per profile comes out: the bio, the exact follower, following and post counts, the verified and private flags, the business category, the public business e-mail and phone when the account publishes them, every link in the bio, and the profile picture.
It reads the same public pages and endpoints Instagram serves to a logged-out visitor. No browser is started, and no account of yours is used.
nasa,@nasa,instagram.com/nasaandhttps://www.instagram.com/nasa/all mean the same thing. Mix them in one list.- Up to 1,000 profiles per run, read several at a time, deduplicated before anything is billed.
- Exact counts rather than the rounded ones the page shows. 20,087,807 followers, not "20M".
- A handle that no longer exists comes back as a free diagnostic row. It doesn't charge you and it doesn't stop the run.
- Empty input returns one labelled free sample row, so you can see the shape before spending anything.
Price
$0.70 per 1,000 profiles.
No volume tiers, no minimum spend, no subscription.
There is one optional extra, off unless you tick it: country, join date and former usernames at $5.00 per 1,000 profiles. Leave the box alone and you pay exactly what you paid before. It is free until 19 September — see below.
What gets charged
- One
profile-scrapedevent per profile row written to the dataset. Nothing else is metered per row. - Free: the sample row an empty run returns, and every diagnostic row (a dead handle, a refused request, a run that ran out of time). All of them carry
"charged": false. - Duplicate handles in the same list are collapsed before the run starts, so you pay once per profile.
- Rows never leave the dataset without a charge and are never charged without a row. The billed event is a named one, so nothing is quietly attached to
apify-default-dataset-item. - One
account-aboutevent per profile, and only if you ticked the box. It is charged whenever the lookup runs, whether or not Instagram turns out to publish a panel for that account — "no published country" is an answer, and it costs the same to find out. If the lookup fails on our side instead, you are not charged for it.
Country, join date and former usernames
Tick "Country, join date and former usernames" and every profile also comes back with where the account is registered, the date it was created, how old that makes it, and any handles it used before. That is Instagram's About this account panel.
Free until 19 September, then $5.00 per 1,000. Two things to know before you rely on it:
- It needs a paid Apify plan. On the free plan the box stays off, the run says so in an uncharged row, and everything else comes back complete.
- Country only shows on larger accounts. Measured across a spread of account sizes: every
account returned a join date, but the smallest ones came back with
country: null. In our own testing the line was there on accounts from a few thousand followers upward and missing on accounts in the hundreds. Instagram decides that, not us, and it can change. Run a small batch of your own handles first and see what you actually get back.
Engagement, worked out for you
Every row now carries an engagement block and a signals block, at no extra cost — they are
arithmetic over the recent posts the profile request already returns, so they cost nothing to add
and nothing to receive.
engagement has average and median likes and comments, an engagement rate as a percentage of
followers, posts per week, when the account last posted and how long ago, what share of recent posts
are video, its most-engaged recent post, and the hashtags and accounts it mentions most.
signals has the follower tier, follower-to-following ratio, whether there is a real contact
route, link count and bio length.
Read them for what they are: they cover the 6 to 12 most recent posts, because that is what
Instagram hands a logged-out reader. A snapshot of current form, not a lifetime average. Every row
carries postsAnalysed so you can judge it. A private account returns no posts, so its block is
nulls with postsAnalysed: 0 — nulls rather than zeros, because no data and no engagement are
different claims and only one of them would be true.
Input
{"usernames": ["nasa","@gopro","https://www.instagram.com/natgeo/"],"includeLatestPosts": false,"maxItems": 50}
| Field | What it does |
|---|---|
usernames | One entry per profile. Handle, @handle or full URL. Up to 1,000 per run, duplicates removed. |
includeLatestPosts | Adds a latestPosts array to every row with the profile's most recent posts: shortcode, link, caption, likes, comments, thumbnail, timestamp. Costs no extra requests and no extra charge. It does make each row a lot bigger. Off by default. |
maxItems | Stop after this many profiles from the list. Leave it out to read everything. Handy while testing, since you pay per profile. |
concurrency | How many profiles are read at once. Default 6, maximum 12. Higher finishes a long list sooner. Lower gives the run more room to fill in every field. |
sessionCookies | Leave empty. See When Instagram refuses a handle. |
proxyUrls | Leave empty. Only for callers who want the traffic to leave through proxy servers they already pay for, as http://user:pass@host:port. |
Run it with empty input and you get one labelled sample row, free, so you can see the output shape before you spend anything.
Output
One row per profile. This is a real row from a real run:
{"ok": true,"charged": true,"recordType": "profile","inputUrl": "https://www.instagram.com/gopro/","id": "28902942","username": "gopro","url": "https://www.instagram.com/gopro/","fullName": "GoPro","biography": "We help the world to capture and share itself in immersive and exciting ways.","externalUrl": "https://gopro.com/","externalUrls": [{ "title": "", "url": "https://gopro.com/", "linkType": "external" }],"followersCount": 20087807,"followsCount": 240,"postsCount": 9551,"verified": true,"private": false,"isBusinessAccount": true,"isProfessionalAccount": true,"categoryName": "Product/service","businessCategoryName": null,"businessEmail": null,"businessPhoneNumber": null,"businessAddress": null,"profilePicUrl": "https://scontent-iad3-1.cdninstagram.com/v/t51.2885-19/449069728_1006900254437011_2641997517452774766_n.jpg","profilePicUrlHD": "https://scontent-iad3-1.cdninstagram.com/v/t51.2885-19/449069728_1006900254437011_2641997517452774766_n.jpg","highlightReelCount": 7,"igtvVideoCount": 0,"hasChannel": false,"joinedRecently": false,"pronouns": [],"relatedProfiles": [],"emailsFound": [],"phonesFound": [],"bioHashtags": [],"bioMentions": [],"latestPosts": [],"fbid": "17841400823952971","dataSource": "profile-api-lean+profile-page","scrapedAt": "2026-08-15T17:41:02.914Z"}
Field notes
usernameis the handle as Instagram spells it, lower-cased. Safe as a key within a run.idis the numeric account id. It survives a rename, so it's the better key if you track accounts over time.followersCountis the exact number, not the abbreviated one on the page.followsCountis how many accounts the profile follows. Instagram publishes this on one surface only, and that surface refuses a share of requests, so on a few profiles it comes backnullinstead of wrong.postsCountis the total at read time.externalUrlis the first link in the bio.externalUrlscarries all of them when the account uses the multi-link layout.businessEmailandbusinessPhoneNumberonly appear when the account publishes them.emailsFoundandphonesFoundadditionally pick up anything written into the bio text.categoryNameis the category the account chose, like "Product/service". Personal accounts have none.profilePicUrlHDis the largest picture Instagram offers. It's a time-limited CDN link, so download the image if you need to keep it.relatedProfilesholds the "suggested for you" accounts, when Instagram offers any.dataSourcesays which public surfaces the row was assembled from. Useful when you want to know why a field is null.scrapedAtis the UTC read time. Counts move constantly, so a row is a snapshot.
Real rows carry "charged": true. Sample rows carry "_sample": true, diagnostic rows carry "_diagnostic": true and an errorCode. Neither is billed.
How it works
Instagram serves several public surfaces to a logged-out visitor: the profile data endpoint the web app calls, the profile page itself, and the public embed card. The run reads whichever ones answer for a given handle.
Those surfaces disagree with each other. Some accounts are refused by one and answered by another. One of them reports a following count that doesn't match the profile page. Another leaves the post count at zero. Each field is taken only from a source known to be right for it, so a field nobody could supply comes back null rather than wrong.
Requests leave through a rotating pool of addresses, so a per-address rate limit is answered by moving instead of waiting. Profiles are read several at a time, each on its own address, which is what keeps a 1,000-handle run to minutes. Nothing is rendered and no headless browser starts, which is most of the reason this costs what it costs.
When Instagram refuses a handle
Sometimes every public surface refuses the same handle inside the same run, usually because the addresses that run happened to draw were all throttled at that moment. You get an uncharged BLOCKED diagnostic row for that handle and the run carries on. Re-running usually picks it up.
If you're pushing large lists and that happens more than you'd like, paste your own Instagram cookie into sessionCookies. The run then retries refused handles with your account, on your own rate limit. It's optional, and most runs never need it. In Chrome: F12 → Application → Cookies → https://www.instagram.com → copy the sessionid value. Pasting the whole cookie string works too.
Use an account you own and are willing to automate. A session Instagram invalidates is dropped for the rest of the run, and the run finishes with what it collected rather than failing. The closing log line carries sessionState: keyless when no cookie was supplied, ok while yours is healthy, degraded after one was dropped, exhausted when none are left.
What people use it for
- Influencer shortlisting. Drop in a few hundred handles and sort by follower count or category before anyone opens Instagram.
- Lead enrichment. Business accounts publish a category, an e-mail and a phone, and the bio often carries a second address. One row per handle, straight into a CRM.
- Weekly tracking. Re-run the same list and diff
followersCountandpostsCountto see who's growing and who's gone quiet. - Market mapping. Read every account in a niche, keep the bios and bio links, cluster them however you like.
- List hygiene.
verified,privateand theNOT_FOUNDdiagnostics tell you which handles are still real.
Reading the output
Three kinds of row come out, and they're easy to tell apart.
- Real rows carry
"charged": trueand"recordType": "profile". One billed event each. - The sample row carries
"_sample": trueand"charged": false. There's exactly one, and only when the input had no handles. - Diagnostic rows carry
"_diagnostic": true,"charged": false, theusernamethey belong to, and anerrorCodeyou can switch on.NOT_FOUNDmeans no public account at that handle.BLOCKEDmeans Instagram refused every public route for it this run.TIME_BUDGETmeans the run ran out of time before reaching it.
Filter on charged == true and you have exactly the rows you paid for. That count always equals the number of billed events.
Private accounts
A private account still returns a full row. Instagram publishes the bio, the counts, the verification flag and the picture for private profiles, and private comes back true. What isn't public is the content: latestPosts is empty for a private account no matter what you ask for. Nothing here reaches behind a private profile.
Limits
- Public data only. Private posts, stories, DMs and follower lists are out of scope, and no input reaches them. Supplying
sessionCookiesdoes not reach any of them either. - Counts are a snapshot. Two runs minutes apart will disagree slightly on a large account, and neither is wrong.
followsCountcomes backnullon a small share of profiles, because the one public surface that publishes it throttles hard. It's never filled in with an approximation.- Instagram currently returns a schema error for a slice of business accounts on its richest public endpoint. Those rows get assembled from the other surfaces instead, which is why
businessCategoryNamecan be null on an account that clearly has a category.categoryNameusually still carries it. latestPostsonly appears when you ask for it, and only covers the most recent handful of posts. For a full post history, use a post-level scraper.- Follower and following lists are not returned. Only the counts.
- Picture and thumbnail links are time-limited CDN URLs. They work right after the run and expire later.
- A renamed handle resolves to whoever owns that handle now. Track
idif that matters. - Hard ceiling of 1,000 profiles per run. Split bigger lists.
- Instagram changes its public surfaces without notice. When it does, a run returns uncharged diagnostic rows rather than wrong data, and the Actor gets repaired.
Questions
Do I need an Instagram account or a cookie?
No. Profiles are read logged out, and that answers almost every handle. sessionCookies is there for the rare handle that gets refused, and it's optional.
What if a username doesn't exist?
One uncharged diagnostic row with errorCode: "NOT_FOUND", and the run carries on through the rest of your list.
Why is followsCount sometimes null?
Instagram publishes it on one public surface only, and that surface throttles hard. The run retries on fresh addresses. When it still refuses, the field stays null instead of taking the wrong number another surface reports. Re-running usually fills it.
Can it get e-mails and phone numbers?
It returns the public business e-mail and phone when the account chose to publish them, plus anything written into the bio text. Personal accounts usually publish neither, and no private contact detail is ever exposed.
Will the run fail if Instagram blocks something?
No. A refused profile becomes an uncharged diagnostic row and the run still finishes as succeeded.
Do I need a proxy?
No. The run brings its own egress and it's already inside the price. proxyUrls exists only for callers who want traffic to leave through servers they already own.
Can I run it on a schedule?
Yes. Nothing is held between runs, so repeating the same input is safe. Diff on id and followersCount to see what moved.