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

The cheapest Instagram profile scraper on the market at $0.70 per 1,000 profiles. Usernames in, one row per public profile out: bio, follower, following and post counts, verified and private flags, business category, e-mail, phone, bio links and HD picture. No login, no browser.

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

2

Total users

1

Monthly active users

17 hours ago

Last modified

Share

Instagram Profile Scraper

Paste Instagram usernames or profile links and get one clean row per public profile: the bio, the follower, following and post counts, the verified and private flags, the business category, the business e-mail and phone when the account publishes them, every link in the bio, and the profile picture. Profiles are read from Instagram's public, logged-out surfaces, with no browser and no account of yours.

  • Usernames, @handles or full profile URLs — mixed together in the same list if you like.
  • Up to 1,000 profiles in a single run, read several at a time, deduplicated before anything is billed.
  • Exact counts, not the rounded ones the page shows: 20,087,807 followers rather than "20M".
  • A profile that no longer exists returns a free diagnostic row, not a charge and not a crash.
  • Empty input returns one clearly labelled free sample row, so you can see the shape before you spend anything.

Price

$0.70 per 1,000 profiles, plus a $0.001 start fee per run (billed per gigabyte of run memory, so exactly that on the default 1 GB).

This is the cheapest Instagram profile scraper on the market, and it is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

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

What is actually 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 blocked target, a dead URL, a search that matched nothing. Those rows all carry "charged": false.
  • Duplicate usernames in the same list are collapsed before the run starts, so you pay once for each profile.
  • Profiles that do not exist, are removed, or that Instagram refused on this run — all uncharged.
  • A run that finds nothing costs the start fee and nothing more.
  • Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to apify-default-dataset-item — the trick that makes some scrapers bill you for their own error messages.

Input

{
"usernames": [
"nasa",
"@gopro",
"https://www.instagram.com/natgeo/"
],
"includeLatestPosts": false
}
FieldWhat it does
usernamesOne entry per profile. nasa, @nasa, instagram.com/nasa and https://www.instagram.com/nasa/ all mean the same thing. Up to 1,000 per run; duplicates are removed.
includeLatestPostsAdds a latestPosts array to every row with the profile's most recent posts — shortcode, link, caption, likes, comments, thumbnail and timestamp. It costs no extra requests and no extra charge; it simply makes each row much larger. Off by default.
maxItemsStop after this many profiles from the list. Leave it out to read the whole list. Handy while testing, since you pay per profile.
concurrencyHow many profiles are read at once. Default 6, maximum 12. Raising it makes a long list finish sooner; lowering it gives the run more room to fill every field.
sessionCookiesLeave empty. Only used on a handle every public surface refused; see When Instagram refuses a handle.
proxyUrlsLeave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as http://user:pass@host:port.

Run it with empty input and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

Output

One row per profile. 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 — the handle as Instagram spells it, lower-cased. Stable, and safe to use as a primary key across runs.
  • id — the numeric account id. It survives a username change, so it is the better key if you track accounts over time.
  • followersCount — the exact number, not the abbreviated one shown on the page.
  • followsCount — how many accounts this profile follows. Instagram publishes this on one surface only, and that surface refuses a share of requests, so on a small number of profiles it can come back null rather than wrong.
  • postsCount — total posts on the profile at read time.
  • externalUrl — the first link in the bio. externalUrls carries all of them when the account uses the multi-link layout.
  • businessEmail — only present when the account publishes a public business e-mail. emailsFound additionally lists any address written into the bio text.
  • categoryName — the category the account chose, e.g. "Product/service". Personal accounts have none.
  • profilePicUrlHD — the largest picture Instagram offers for the account. The link is a time-limited CDN URL — download the image if you need to keep it.
  • relatedProfiles — the "suggested for you" accounts Instagram associates with this one, when it offers any.
  • dataSource — which public surfaces the row was assembled from. Useful when you want to know why a field is null.
  • scrapedAt — when the row was read, in UTC. Counts move constantly, so a row is a snapshot, not a fact.

Every real row carries "charged": true. Sample rows carry "_sample": true and diagnostic rows carry "_diagnostic": true with an errorCode you can filter on, and neither is ever billed.

How it works

  • It reads the public, logged-out surfaces Instagram serves to anyone with a browser: the profile data endpoint the web app calls, the profile page itself and the public embed card. That is the default path and it is what answers almost every 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 does not match the profile page, and another leaves the post count at zero. The run reads whichever ones answer for a given account and takes each field only from a source that is known to be correct for it — a field nobody could supply comes back null rather than wrong.
  • Requests leave through a large pool of rotating addresses, so a per-address rate limit is answered by moving to another address rather than by waiting.
  • Profiles are read several at a time, each on its own address, which is what keeps a 1,000-profile run to minutes rather than hours.
  • No page is rendered and no headless browser is started, which is most of the reason this costs what it costs.
  • Only when all of those refuse the same handle on the same run does it try once more with a session. See below.

When Instagram refuses a handle

Sometimes every public surface refuses the same handle within the same run — usually because the addresses that run happened to draw were all throttled at that moment. Before giving up and writing an uncharged BLOCKED row, the run makes one more attempt using a built-in session.

Two things worth knowing about that fallback:

  • It is a last resort, not the normal path. Rows assembled without it are the overwhelming majority; you can tell them apart by dataSource, which reads profile-api-session on the rare row that needed it.
  • The built-in session is shared between everyone running this Actor, and Instagram meters a session per account rather than per caller. Under heavy concurrent load it is therefore a small, shared budget, not an unlimited one. If you are running large lists and want that fallback to be reliably available to you, put your own cookie in sessionCookies: your account, your rate limit, nobody else drawing on it.

To get your own cookie 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 that Instagram invalidates is dropped for the rest of the run and the run finishes with whatever it collected rather than failing.

The closing log line of every run carries sessionState: keyless when no session was configured at all, ok while sessions are healthy, degraded after one was dropped, and exhausted when none are left.

What people use it for

  • Influencer shortlisting: drop in a few hundred handles and sort the result by follower count, engagement potential or category before anyone opens Instagram.
  • Lead enrichment: business accounts publish a category, an e-mail and a phone number, and the bio often carries a second address. One row per handle, straight into a CRM.
  • Brand monitoring: re-run the same list weekly and diff followersCount and postsCount to see who is growing and who has gone quiet.
  • Market mapping: read every account in a niche, keep the bios and the bio links, and cluster them however you like.
  • Data quality checks on a list you already own: verified, private and the not-found diagnostics tell you which handles are still real.
  • Feeding a dashboard or a spreadsheet with typed, flat rows rather than screenshots of a rendered page.

Reading the output

Every run writes three kinds of row, and they are 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 is exactly one, it only appears when the input had no usernames, and it exists so you can see the shape of the output before you spend anything.
  • Diagnostic rows carry "_diagnostic": true, "charged": false, the username they belong to and an errorCode you can switch on: NOT_FOUND when no public account exists at that handle, BLOCKED when Instagram refused every public route for it on this run, TIME_BUDGET when the run ran out of time before reaching it.

If you only want the data, filter on charged == true. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.

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 is not public is the content: latestPosts is empty for a private account no matter what you ask for. Nothing in this Actor can see behind a private profile, and nothing about it tries to.

Limitations

  • Public data only. Private posts, stories, direct messages, follower lists and anything behind a login are out of scope and no input can reach them. Supplying sessionCookies does not unlock any of them — it only affects the retry described above.
  • The counts are a snapshot at read time. Followers move by the second on a large account, so two runs minutes apart will disagree slightly, and neither is wrong.
  • Instagram publishes the following count on a single public surface, and that surface refuses a share of requests. The run retries it on fresh addresses, but on a small number of profiles followsCount still comes back null. It is never filled in with an approximation.
  • Instagram itself currently returns a schema error for a slice of business accounts on its richest public endpoint. Those rows are 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.
  • Post captions, likes and comments are only included when you ask for includeLatestPosts, and only the most recent handful of posts. For a full post history, use a post-level scraper instead.
  • Follower and following LISTS are not returned — only the counts.
  • Profile picture and post image links are time-limited CDN URLs. They work immediately after the run and expire later, so download anything you need to keep.
  • A handle that has been renamed resolves to whoever owns the handle now. Track id if that matters to you.
  • Hard ceiling of 1,000 profiles per run. Split larger lists across runs.
  • Instagram changes its public surfaces without notice. When that happens a run returns uncharged diagnostic rows rather than wrong data, and the Actor is repaired.

Questions

Do I need an Instagram account, cookies or a session?

No. Profiles are read logged-out, and that is what answers almost every handle. A session is used only as a last resort on a handle every public surface refused, and one is built in, so you never have to supply anything. Adding your own cookie in sessionCookies is optional and only changes which session that fallback uses.

What happens if a username does not exist?

You get one uncharged diagnostic row with errorCode: "NOT_FOUND" and the run carries on to the rest of your list. You are never billed for a handle that returned nothing.

Why is followsCount sometimes null?

Because Instagram publishes the following count on one public surface only, and that surface throttles hard. The run retries it on several fresh addresses; when it still refuses, the field is left null instead of being filled with the wrong number that 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 (emailsFound, phonesFound). Personal accounts usually publish neither, and no private contact detail is ever revealed.

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 is already inside the price you see. The proxyUrls field exists only for callers who specifically want traffic to leave through servers they already own.

Can I run this on a schedule?

Yes. Nothing is held between runs, so the same input is safe to repeat. Diff on id and followersCount to see what changed.

How do I get exactly the rows I paid for?

Filter the dataset on "charged": true. Sample and diagnostic rows are always false, and the number of charged rows always equals the number of billed events.