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

Making an Instagram account private hides the posts and nothing else. The bio, the picture and all three counts stay public. Either kind is one row: exact follower, following and post counts, verified flag, business category, e-mail, phone, bio links. $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

18

Total users

5

Monthly active users

20 hours

Issues response

2 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. On top of that, $0.001 per run as the platform start fee, billed per gigabyte of run memory, so exactly $0.001 on the default 1 GB.

No volume tiers, no minimum spend, no subscription.

ProfilesTotal
100$0.071
1,000$0.701
10,000$7.001
100,000$70.001

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.
  • A run that finds nothing costs the start fee and nothing else.
  • 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.

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. A failed run would still bill the start fee, which would mean paying to be told something went wrong.

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.