Instagram Profile Scraper - $0.70 per 1,000 Profiles avatar

Instagram Profile Scraper - $0.70 per 1,000 Profiles

Pricing

from $0.70 / 1,000 profile scrapeds

Go to Apify Store
Instagram Profile Scraper - $0.70 per 1,000 Profiles

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

Dami's Studio

Maintained by Community

Actor stats

0

Bookmarked

47

Total users

29

Monthly active users

5 days ago

Last modified

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/nasa and https://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-scraped event 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-about event 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
}
FieldWhat it does
usernamesOne entry per profile. Handle, @handle or full URL. Up to 1,000 per run, duplicates removed.
includeLatestPostsAdds 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.
maxItemsStop after this many profiles from the list. Leave it out to read everything. Handy while testing, since you pay per profile.
concurrencyHow 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.
sessionCookiesLeave empty. See When Instagram refuses a handle.
proxyUrlsLeave 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

  • username is the handle as Instagram spells it, lower-cased. Safe as a key within a run.
  • id is the numeric account id. It survives a rename, so it's the better key if you track accounts over time.
  • followersCount is the exact number, not the abbreviated one on the page.
  • followsCount is 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 back null instead of wrong.
  • postsCount is the total at read time.
  • externalUrl is the first link in the bio. externalUrls carries all of them when the account uses the multi-link layout.
  • businessEmail and businessPhoneNumber only appear when the account publishes them. emailsFound and phonesFound additionally pick up anything written into the bio text.
  • categoryName is the category the account chose, like "Product/service". Personal accounts have none.
  • profilePicUrlHD is the largest picture Instagram offers. It's a time-limited CDN link, so download the image if you need to keep it.
  • relatedProfiles holds the "suggested for you" accounts, when Instagram offers any.
  • dataSource says which public surfaces the row was assembled from. Useful when you want to know why a field is null.
  • scrapedAt is 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 followersCount and postsCount to 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, private and the NOT_FOUND diagnostics 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": true and "recordType": "profile". One billed event each.
  • The sample row carries "_sample": true and "charged": false. There's exactly one, and only when the input had no handles.
  • Diagnostic rows carry "_diagnostic": true, "charged": false, the username they belong to, and an errorCode you can switch on. NOT_FOUND means no public account at that handle. BLOCKED means Instagram refused every public route for it this run. TIME_BUDGET means 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 sessionCookies does 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.
  • followsCount comes back null on 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 businessCategoryName can be null on an account that clearly has a category. categoryName usually still carries it.
  • latestPosts only 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 id if 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.