Instagram Profile and Posts Scraper, Full History, No Login avatar

Instagram Profile and Posts Scraper, Full History, No Login

Pricing

from $1.00 / 1,000 profile scrapeds

Go to Apify Store
Instagram Profile and Posts Scraper, Full History, No Login

Instagram Profile and Posts Scraper, Full History, No Login

Scrape public Instagram profiles with their full post history, not only the latest 12 posts. Profile stats, bio links, every post with caption and timestamp, optional likes, comments and views. No login, no cookies, no browser. Never billed for private or missing accounts or duplicates.

Pricing

from $1.00 / 1,000 profile scrapeds

Rating

0.0

(0)

Developer

Scrapegeist

Scrapegeist

Maintained by Community

Actor stats

0

Bookmarked

9

Total users

1

Monthly active users

3 days ago

Last modified

Share

Scrape public Instagram profiles with their full post history, not only the latest 12 posts. One profile row per account with stats and bio links, one row per post with caption, type, thumbnail and timestamp, and likes, comments and views when you ask for them. Hashtag pages, location pages and single post URLs work too. No login, no cookies, no browser.

Posts table of a run over organisation accounts at detail level: thumbnail, account, timestamp, type, caption, likes, comments


What you get in 30 seconds

Press Start with the prefilled input. It asks for two organisation accounts and nothing else:

{
"usernames": ["natgeo", "https://www.instagram.com/sfmoma/"]
}

The run gives you, per account:

  • one profile row: username, full name, biography, bio links, follower and following count, verified and private flags, profile picture, pronouns and highlight titles. Set detailSample above 0 and it also carries an engagement rate computed from a sample of the newest posts;
  • up to 100 post rows (maxPostsPerProfile, raise it to 5,000): caption, media type, product type, thumbnail, timestamp, post URL and the account that owns it. The first 12 posts are included in the profile price.

Every row carries scraped_at or a taken_at you can sort by, and discoveredVia, which says what brought the row into the dataset (input, profile:natgeo, hashtag:museum, location:212988663).

The dataset has four views in the Apify Console: Profiles, Posts, Locations and Skipped. Export any of them as JSON, CSV, Excel or XML.

Input kinds, with examples

You can mix all five in one run. Each has its own cap.

FieldWhat you pasteWhat comes back
usernamesnatgeo, @sfmoma, https://www.instagram.com/britishmuseum/One profile row plus its post history
postUrlshttps://www.instagram.com/p/DdG4RIxIPyf/, or the bare shortcode DdG4RIxIPyfOne detail row per post
hashtagsmuseum, #architectureAbout 60 post rows per tag (see Limits)
locations212988663 (New York), 213385402 (London), or /explore/locations/213385402/One location row plus about 70 post rows
searchQueriesany keywordA skipped row. Keyword search needs a login (see Limits)
{
"usernames": ["natgeo", "sfmoma", "britishmuseum"],
"postUrls": ["https://www.instagram.com/p/DdG4RIxIPyf/"],
"hashtags": ["museum"],
"locations": ["213385402"],
"maxPostsPerProfile": 200,
"postDetail": "list",
"detailSample": 3
}

Handles that do not exist and invalid entries do not stop the run. They become free skipped rows with a reason you can filter on.

A private account is the one case that is not free: Instagram publishes its profile card to anyone, so you get a normal profile row with the name, follower and following count, biography and bio links, charged as a profile row, plus a free skipped row with the reason private_no_posts. The posts are not public, so none are delivered and the engagement fields stay null.

Sample output

The run's Output tab has one table per row type (Posts, Profiles, Locations, Skipped). Apify tables cannot filter rows, so every table lists every row, and a row of another type shows undefined in the columns it does not have. Download the JSON or CSV and filter on type for a clean per-type file.

Four real rows, from a run on organisation accounts. Long CDN URLs are shortened here with ...; the dataset carries them in full.

Profile row

{
"type": "profile",
"username": "natgeo",
"pk": "787132",
"url": "https://www.instagram.com/natgeo/",
"full_name": "National Geographic",
"biography": "Step into wonder and find your inner explorer with National Geographic 🌎",
"bio_links": [
{ "url": "http://visitstore.bio/natgeo", "title": null },
{ "url": "https://ngmdomsubs.nationalgeographic.com/servlet/OrdersGateway?cds_mag_code=NGM", "title": "Subscribe Here!" }
],
"external_url": "http://visitstore.bio/natgeo",
"follower_count": 268532277,
"following_count": 194,
"post_count": null,
"is_verified": true,
"is_private": false,
"profile_pic_url": "https://scontent.cdninstagram.com/v/t51.82787-19/683576066_...jpg",
"pronouns": [],
"highlights": ["Jaguar vs. Croc"],
"engagement_rate": 0.0003,
"avg_likes": 70596,
"avg_comments": 408,
"avg_video_views": 768614,
"sample_size": 3,
"scraped_at": "2026-09-18T09:12:44.000Z",
"discoveredVia": "input"
}

Post row, list level (the default)

{
"type": "post",
"pk": "3983118388705688735",
"shortcode": "DdG4RIxIPyf",
"url": "https://www.instagram.com/p/DdG4RIxIPyf/",
"owner_username": "natgeo",
"owner_pk": "787132",
"media_type": "video",
"product_type": "clips",
"caption": "Presented by @Rolex. Welcome to Africa 🌍 Africa Earth's Wild Home takes you deep into the heart of our wildest continent ...",
"thumbnail_url": "https://scontent-tpe5-1.cdninstagram.com/v/t51.82787-15/802503979_...jpg",
"taken_at": "2026-09-10T12:52:15.035Z",
"detail_level": "list",
"discoveredVia": "profile:natgeo"
}

Post row, engagement level

Likes, comment count and the view count of a video, at about 55 KB per post instead of the 88 KB of a full detail row.

{
"type": "post",
"pk": "3947535193527528320",
"shortcode": "DbIdlf0j6uA",
"url": "https://www.instagram.com/p/DbIdlf0j6uA/",
"owner_username": "natgeo",
"owner_pk": "787132",
"media_type": "video",
"product_type": "clips",
"caption": "Describe Pompeii: Out of Time in 3 words? It's Now Streaming.",
"thumbnail_url": "https://scontent-tpe5-1.cdninstagram.com/v/t51.82787-15/752808040_...jpg",
"taken_at": "2026-07-23T10:34:47.618Z",
"detail_level": "engagement",
"discoveredVia": "profile:natgeo",
"like_count": 70603,
"comment_count": 408,
"view_count": 768770,
"taken_at_exact": null,
"video_url": "https://instagram.fkhh1-2.fna.fbcdn.net/o1/v/t2/f2/m86/...mp4",
"usertags": [],
"location": null,
"co_authors": [],
"comments": [],
"like_and_view_counts_disabled": false
}

Post row, detail level

Everything the post page itself carries: exact timestamp, usertags, co-authors, location and the visible comments. The post page publishes no view count, so view_count is null here. The one exception is a post URL run with minViews, where the embed page is read first and its view count is carried over into the detail row.

{
"type": "post",
"pk": "3983118388705688735",
"shortcode": "DdG4RIxIPyf",
"url": "https://www.instagram.com/p/DdG4RIxIPyf/",
"owner_username": "natgeo",
"owner_pk": "787132",
"media_type": "video",
"product_type": "clips",
"caption": "Presented by @Rolex. Welcome to Africa 🌍 ...",
"thumbnail_url": "https://scontent-tpe5-1.cdninstagram.com/v/t51.82787-15/802503979_...jpg",
"taken_at": "2026-09-10T12:52:15.035Z",
"detail_level": "detail",
"discoveredVia": "input",
"like_count": 70358,
"comment_count": 408,
"view_count": null,
"taken_at_exact": "2026-09-10T13:00:14.000Z",
"video_url": "https://scontent-tpe5-1.cdninstagram.com/o1/v/t2/f2/m86/...mp4",
"usertags": ["rolex"],
"location": null,
"co_authors": ["rolex"],
"comments": [
{ "pk": "18126357995498209", "username": "...", "text": "...", "created_at": "2026-09-17T13:43:16.000Z", "like_count": 0 }
],
"like_and_view_counts_disabled": false
}

A location input adds a fifth shape, the location row:

{
"type": "location",
"id": "212988663",
"name": "New York, New York",
"lat": 40.714,
"lng": -74.005,
"nearby": [{ "id": "378453", "name": "The Metropolitan Museum of Art", "lat": 40.779, "lng": -73.963 }],
"discoveredVia": "input"
}

And a skipped row, for anything that could not be delivered:

{ "type": "skipped", "input": "sfmoma_does_not_exist", "reason": "not_found", "message": "profile: null_user" }

reason is one of not_found, private_no_posts, login_wall, rate_limited, identity_mismatch, invalid_input, not_available_logged_out, error.

What it costs

This actor bills per row (pay per event). The current prices are in the pricing box at the top of this page. There is no actor start fee: a run that delivers nothing costs nothing. Compute and the residential proxy are included in the row prices and are never billed on top.

Row typeCharge eventWhat it covers
Profile rowprofile-scrapedThe profile row and the first 12 posts of that account, delivered as free list rows. The detailSample posts among those 12 are the exception: they are fetched for the engagement rate and charged at the level they are fetched at. detailSample is 0 by default, so nothing extra is charged unless you ask for it.
Timeline post row, list levelpost-scrapedA post from a profile timeline beyond the first 12. Caption, type, product type, thumbnail, timestamp, post URL and the account that owns it.
Hashtag or location row, list levelgrid-post-scrapedThe same fields, off a hashtag or a location grid, plus whatever counts that grid printed for free: a view count on hashtag rows, likes, comment count and the exact timestamp on location rows. Its own event, because one grid page delivers many rows and costs less per row than a timeline page.
Post row, engagement levelpost-engagement-scrapedA post with likes, comment count and the view count. From a timeline, a hashtag or location grid, or postUrls.
Post row, detail levelpost-detail-scrapedA post with likes, comment count, exact timestamp, video URL, usertags, location and the visible comments. From a timeline, a hashtag or location grid, or postUrls.
Location rowlocation-scrapedThe place with its coordinates, plus the other places its posts are tagged at. Priced like a profile row.

Charged by the level you asked for and by where the row came from, never by what arrived. A timeline list row is post-scraped, a hashtag or location list row is grid-post-scraped, an engagement row is post-engagement-scraped and a detail row is post-detail-scraped. With the default postDetail: list, hashtag and location rows stay list rows at the grid price, even though their grid hands over likes, the comment count, the exact timestamp or a view count for free. A number the page volunteered does not raise your bill. Ask for postDetail: detail and those rows become detail rows, with the detail price.

What you are never charged for

  • Skipped rows. Every skipped row is free, whatever the reason.
  • Duplicates. A post is delivered once per run and billed once, even when two hashtags and a profile all contain it.
  • Accounts that do not exist, and every other invalid input.
  • Login walls and rate limits. If Instagram gates a request, you get a skipped row, not a bill. A place whose grids were all gated still gives you its location row, free, next to that skipped row.
  • Rows dropped by minViews. The fetch that read the view count is our cost, not yours.
  • Empty runs. No rows, no charge, and no start fee.
  • The first 12 posts of every profile. They come with the profile row, as list rows. detailSample is 0 by default, so none of them costs you anything extra. Raise it and the N newest of those posts are fetched for the engagement rate and charged at the level they are fetched at.
  • Comments. They are part of a detail row and are never billed separately.
  • Any row after your charge limit is reached. When Apify reports that the limit is hit, the run stops fetching and hands you the page-1 posts the profile row already paid for, as free list rows.

maxTotalPosts is a hard ceiling on billed post rows: the four post events together. It is checked before each fetch, not after the row lands, so the number you type is the most post rows you can be charged for in that run. If your Apify charge limit is lower, the actor lowers maxTotalPosts to match it and says so in the log.

Honest limits

Everything below was measured on 2026-09-18 from a logged-out reader. This actor does not log in, so these are the real edges of the public surface, not settings you can raise.

Hashtag pages give about 60 posts per tag. That is where Instagram's public tag page ends: it keeps paging until it reports that there is no next page, which happened after 60 posts for tedx and 64 for museum. A higher maxResultsPerHashtag gives you no more. The logged-out tag grid also contains reels only, no photo posts: all 172 posts measured across two tags were videos. For photo posts from an account, scrape the account itself through usernames.

Location pages give about 70 posts per place. The location page does not page. Its grid answers "there is more" and then serves an empty page for the cursor, so the actor reads the two grids the page itself has (the ranked one and the newest one) and stops there. Measured: 69 posts for New York (212988663) and 72 for The Metropolitan Museum of Art (378453), with almost no overlap between the two grids. A location id nobody uses comes back as a skipped row with not_found.

View counts exist for videos and reels only. Instagram publishes no view count for an image or a carousel post to a logged-out reader, on any route. Those posts have view_count: null and never pass minViews. The view_count field holds the number Instagram itself publishes: the play count for a reel and for every hashtag row, the video view count for a feed video read from its embed page. Both live in one field, so a comparison across the two kinds compares two metrics.

A detail row carries up to about 14 comments. The post page prints them inline and says there is a next page, but every pagination variable is dropped and the same 14 come back (measured on a post with 408 comments). comment_count is the real total, so you always see how many a post has.

Keyword search needs a login. Use hashtags and locations for discovery. A keyword in searchQueries comes back as a skipped row with the reason not_available_logged_out and is never billed.

Stories and highlight contents need a login. Highlight titles are on the profile row. The story routes answer a login page, and the highlights query answers an empty list to a logged-out reader, so there is nothing to deliver.

Not served to a logged-out reader at all, so not returned: business contact fields (email, phone, WhatsApp, category), follower and following lists, and the total post count (post_count is null).

Hidden counts come back as null, never 0. When an account hides its like and view counts, the row carries like_and_view_counts_disabled: true and null counts, so you can tell "hidden" from "zero".

How to

Get the full post history, not 12 posts

Set maxPostsPerProfile to how far back you want to go, up to 5,000. The actor pages the profile timeline 12 posts at a time and stops at your number, at maxTotalPosts, or when the account runs out of posts.

{ "usernames": ["sfmoma"], "maxPostsPerProfile": 2000 }

Get an engagement rate per profile

detailSample is 0 by default, so the sample is off and the engagement fields stay null. Set it and the N newest posts of each account are fetched at a level that carries counts, which writes engagement_rate, avg_likes, avg_comments, avg_video_views and sample_size onto the profile row. The rate is (avg_likes + avg_comments) / follower_count, rounded to four decimals, and is null when the sample has no counts or the follower count is hidden. Each sampled post is charged as a post engagement or a post detail row, so a sample of 3 is what most runs need.

Only posts since a date

{ "usernames": ["britishmuseum"], "since": "2026-01-01" }

Profile pagination stops once the posts are older than the date. Hashtag and location grids are ranked, not sorted by date, so there since drops the older posts out of the grid and the actor keeps reading the rest.

Only videos above a view threshold

{ "usernames": ["natgeo"], "minViews": 500000 }

A match from a profile is delivered as an engagement row (the embed page is the only source of its view count). A match from a hashtag grid stays at the level you asked for, because the grid prints the count. A match from a location grid is an engagement row when the grid printed no view count for it. A match from postUrls is a detail row, because a post URL is always fetched at detail level. Posts below the threshold are not delivered and not billed. Image and carousel posts have no view count anywhere, so they are dropped without a request. On a hashtag page the view count is already in the grid, so the filter there costs nothing extra.

Scrape hashtags

{ "hashtags": ["museum", "architecture"], "maxResultsPerHashtag": 60 }

Rows arrive with discoveredVia: "hashtag:museum" and a view_count the grid printed, at the plain list price. Add "postDetail": "detail" to fetch each post on top for likes, the exact timestamp, usertags, the location and the comments, at the detail price.

Scrape locations

{ "locations": ["212988663", "213385402"] }

You get one location row per place, with its coordinates and a nearby list, and about 70 post rows that already carry likes, the comment count and the exact timestamp. They stay list rows at the list price: the grid printed those counts for free, so they do not raise the bill. The exception is minViews, which buys an embed per video and delivers those as engagement rows.

nearby is not a nearby-places list. Instagram publishes no such list to a logged-out reader. nearby holds the other places that the posts on this page are tagged at, with their coordinates, which is the only place data the public page exposes. A city page therefore lists many of them, a single venue usually none, because its posts all name the venue itself.

To find a location id, open the place on instagram.com and read it out of the URL: instagram.com/explore/locations/213385402/london-united-kingdom/.

Use your own proxy

The default is Apify Proxy with the RESIDENTIAL group, which needs no setup. Instagram gates datacenter IPs on the post routes, so residential is required. Your own proxy URLs are accepted in the proxy editor.

{ "proxyConfiguration": { "useApifyProxy": false, "proxyUrls": ["http://user:pass@my-proxy:8000"] } }

API and integrations

Python

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("scrapegeist/instagram-profile-posts-scraper").call(run_input={
"usernames": ["natgeo", "sfmoma"],
"maxPostsPerProfile": 100,
"detailSample": 3,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["type"] == "profile":
print(item["username"], item["follower_count"], item["engagement_rate"])

JavaScript

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('scrapegeist/instagram-profile-posts-scraper').call({
usernames: ['natgeo', 'sfmoma'],
maxPostsPerProfile: 100,
detailSample: 3,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const posts = items.filter((i) => i.type === 'post');
console.log(`${posts.length} posts`);

cURL

curl -X POST "https://api.apify.com/v2/acts/scrapegeist~instagram-profile-posts-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
-H 'Content-Type: application/json' \
-d '{"usernames":["natgeo"],"maxPostsPerProfile":60,"postDetail":"list"}'

n8n, Make, Zapier and MCP

The actor is a normal Apify actor, so every Apify integration drives it without extra work. In n8n use the Apify node, pick this actor and paste the JSON input. In Make use the Apify "Run an Actor" module and map the dataset items into the next step. In Zapier use the Apify app with the "Run Actor" action. Through the Apify MCP server the actor is a callable tool, so an assistant can start a run and read the dataset back without any code. You can also let Apify push the dataset straight into Google Sheets, Airtable, Slack or a webhook after every run.

For AI agents

Call this actor when you need public Instagram profile or post data and you have no Instagram account to use.

  • Actor id: scrapegeist/instagram-profile-posts-scraper.
  • Minimal input: {"usernames": ["natgeo"]}. Nothing else is required.
  • Output is a flat array of objects. Branch on the type field: profile, post, location, skipped. Do not assume all rows have the same shape.
  • Post rows have a detail_level field: list, engagement or detail. Read like_count, comment_count and view_count only when detail_level is not list, or on hashtag and location rows.
  • A null count means "not published", never "zero". Check like_and_view_counts_disabled before you report a zero.
  • Never retry a skipped row with reason not_found, private_no_posts or not_available_logged_out. Those do not change on a second run. A login_wall or rate_limited row is worth one retry later.
  • Bound spend with maxTotalPosts and maxPostsPerProfile before you start a run over a long list.

Switching from the official Instagram actor

The field names differ. This map covers the ones people use most:

Official actorThis actorNote
usernameusernameSame
fullNamefull_name
biographybiographySame
followersCountfollower_count
followsCountfollowing_count
postsCountnot availableInstagram does not serve the total to a logged-out reader. It is post_count: null.
verifiedis_verified
privateis_private
profilePicUrlprofile_pic_url
externalUrlexternal_urlbio_links carries all of them, not only the first
latestPosts[]separate post rowsOne row per post in the same dataset, with type: "post" and discoveredVia: "profile:<username>". Filter on that instead of reading a nested array, and you are no longer capped at 12.
latestPosts[].captioncaption
latestPosts[].likesCountlike_countNeeds postDetail engagement or detail, or detailSample above 0
latestPosts[].commentsCountcomment_countSame
latestPosts[].videoViewCountview_countVideos and reels only
latestPosts[].timestamptaken_at, taken_at_exacttaken_at is derived from the post id and is on every row. taken_at_exact is the value Instagram prints, on detail rows, on location rows, and on engagement rows where the grid printed it.
latestPosts[].urlurl
latestPosts[].typemedia_typeimage, video, carousel or unknown

Three behaviour differences worth knowing before you migrate:

  1. The profile row and the posts are separate rows, not one nested document. That keeps a run of 5,000 posts flat and exportable to CSV.
  2. Failures are rows, not gaps. A handle that does not exist produces a skipped row you can join back to your input list.
  3. Nothing is guessed. A value Instagram does not publish is null, never 0 and never an estimate.

Input reference

FieldTypeDefaultWhat it does
usernamesstring[]["natgeo", "https://www.instagram.com/sfmoma/"] (prefill)Handles, @handles or profile URLs. One profile row per account plus its posts.
hashtagsstring[][]Hashtags with or without #. About 60 posts per tag, reels only.
locationsstring[][]Location ids or /explore/locations/<id>/ URLs. One location row plus about 70 posts.
searchQueriesstring[][]Keywords. Needs a login, so each one returns a free skipped row.
postUrlsstring[][]Post or reel URLs or shortcodes. Each becomes a detail row, whatever postDetail says. With minViews, a view count is added from the embed page (videos and reels only).
maxPostsPerProfileinteger1000 to 5,000. How far back to page each profile. 0 returns the profile row only.
maxTotalPostsinteger10000Hard cap on billed post rows across all inputs (the four post events together), checked before each fetch.
maxResultsPerHashtaginteger100Cap per hashtag and per location page. The public pages end before 100 anyway.
postDetailenumlistlist, engagement or detail. See the level table below.
detailSampleinteger00, the default, disables the sample. Above 0, and with postDetail: list, the N newest posts per profile are fetched for the engagement rate and charged at the level they are fetched at.
sincestring (date)noneISO date, for example 2026-01-01. Paging stops at posts older than this.
minViewsintegernoneKeep only posts with at least this many views. Videos and reels only. Dropped rows are not billed.
includeCommentsbooleantruePut the visible comments (about 14) on each detail row. Never billed separately.
proxyConfigurationobjectApify Proxy, RESIDENTIALResidential is required for post pages. Your own proxy URLs are accepted.
concurrencyinteger41 to 8 parallel sessions. Each keeps one proxy IP and one cookie jar.

The three post detail levels

LevelExtra request per postFields it addsBilled as
listnoneCaption, media type, product type, thumbnail, taken_at, URL, ownerpost-scraped from a timeline, grid-post-scraped off a hashtag or location grid
engagementabout 55 KBlike_count, comment_count, view_count, video_urlpost-engagement-scraped
detailabout 88 KBlike_count, comment_count, taken_at_exact, video_url, usertags, co_authors, location, comments, carousel_media. No view count.post-detail-scraped

Use engagement when you need views, detail when you need comments, usertags or the location, and list when you need reach and speed.

FAQ

Do I need an Instagram login, cookies or a session id? No. The actor reads only what a logged-out visitor sees. There is nothing to configure and nothing of yours to put at risk.

Why do I only get about 60 posts from a hashtag? Because that is where Instagram's public tag page ends. The actor pages until the page itself says there is no next page. Raising maxResultsPerHashtag changes nothing. The same holds for locations at about 70 posts.

Can I get more than 14 comments on a post? No, not without a login. The post page prints about 14 and then drops every pagination attempt. comment_count still gives you the real total.

Why is view_count null on an image post? Instagram publishes a view count for videos and reels only. There is no logged-out route that carries one for an image or a carousel post, so the field stays null rather than guessing.

Why is post_count null? The total post count of an account is not served to a logged-out reader. The actor returns null instead of an estimate.

Can I search Instagram by keyword? No. Keyword search needs a login. Use hashtags and locations for discovery.

Can I get stories or highlight media? No. Both need a login. The profile row carries the highlight titles, which is everything the public surface exposes.

Can I get an email or a phone number from a profile? No. Business contact fields are behind the login. If you need contact data for organisations, the Facebook Pages Contact Scraper reads the public contact block of a Facebook Page instead.

What happens with private accounts? You get the profile row, charged as a profile row, because Instagram shows the profile card of a private account to anyone: name, follower and following count, biography, bio links, verified and private flags. On top of it comes a free skipped row with the reason private_no_posts. What you do not get is the posts, because they are not public, so the engagement rate and the averages stay null. Put another way, you pay for a private account only when there was something to deliver, and never for its posts.

Why a residential proxy? Instagram gates datacenter IPs on the post routes. The default RESIDENTIAL group of Apify Proxy is enough and needs no setup.

A run failed. What now? Re-run it. Failed and skipped rows are never billed, so a failed run costs you nothing. When Instagram gates the whole proxy pool, the actor stops early and says so in the status message instead of burning your budget on walls.

Can I run this over thousands of accounts? Yes. Use maxTotalPosts to bound spend, and Apify's scheduler to split the work. The actor streams rows to the dataset as it goes, so a stopped run keeps everything it already delivered.

This actor reads only what a logged-out visitor sees on public Instagram pages. No login, no cookies, no account, no CAPTCHA solving, no circumvention of a technical access control. The posts of a private account are not public, so they are never read: only the profile card Instagram itself shows to every visitor.

The output is research, enrichment and verification data. It is not an opt-in marketing list, and it does not become one because it is in a dataset. If you contact anyone in it, you are the controller for that processing, and you are responsible for the rules that apply to you: GDPR, UWG § 7 in Germany, CAN-SPAM in the US, PECR in the UK, and the equivalents elsewhere. Check before you send.

Every row carries discoveredVia and a timestamp, so you can show where a record came from and when.

Data notice for account owners

What is collected: the public profile of an Instagram account (username, name, biography, bio links, follower and following count, verified and private flags, profile picture, pronouns, highlight titles) and its public posts (caption, media type, thumbnail, timestamp, and where public, likes, comment count, view count, usertags, location and the comments the post page prints). Nothing behind the login is read. Follower and following lists, stories, direct messages and the posts of private accounts are never read. Of a private account only the public profile card is collected, which is what Instagram shows to any logged-out visitor.

Who processes it and why: the customer who starts a run chooses the input and receives the output, and is the controller for any use of it. The operator of this actor processes the data only to deliver it to that customer and keeps no database beyond the run storage the customer configures. Legal basis for the collection is legitimate interest (GDPR Art. 6(1)(f)): the data is published by the account owner for a public audience, it is limited to what a logged-out visitor sees, and no contact details are collected.

Your rights: to have your account excluded from future runs, to learn whether it was included, or to have data erased on the operator's side, open an issue on this actor's Store page (the Issues tab) with the subject "Exclude my account" and the profile URL. Exclusions are applied within 30 days and checked on every run. You also have the right to complain to a supervisory authority.

Other Scrapegeist actors

Support

Use the Issues tab on this actor's Apify Store page for bugs and feature requests. Include the profile URL, the hashtag or the location id when a field is missing: that is what makes a fix possible. Failed and skipped rows are never billed, so a broken run costs you nothing while we investigate.