Instagram Scraper: Profiles, Post History & Reels, No Login
Pricing
from $0.24 / 1,000 timeline posts
Instagram Scraper: Profiles, Post History & Reels, No Login
Instagram scraper for the full post history of public profiles, not only the latest 12 posts. Every post with caption, type, thumbnail and timestamp, reels with plays. Likes, comment counts and comment texts at the detail level. No login, no cookies. From $0.28 per 1,000 posts.
Pricing
from $0.24 / 1,000 timeline posts
Rating
0.0
(0)
Developer
Scrapegeist
Maintained by CommunityActor stats
0
Bookmarked
407
Total users
25
Monthly active users
a day ago
Last modified
Categories
Security
Limited permissionsShare
Instagram Scraper: Profiles, Full Post History and Reels, No Login
Scrape public Instagram profiles with their full post history, not only the latest 12 posts, for a fraction of the official actors' price. One profile row per account with stats and bio links, one row per post with caption, type, thumbnail and timestamp, and plays on every reel. Likes, comment counts, views and comment texts when you ask for them. Hashtag pages and single post URLs work too. No login, no cookies, no browser.

Why this one
The same public data as the official Apify Instagram actors, with the full post history from $0.24 per 1,000 posts and no limit of 12 posts. Since 2026-10-23 Instagram no longer prints likes and comment counts on a logged-out profile timeline, so a post with its counts is a detail row (see Honest limits).
Prices per 1,000 rows, Free / Bronze / Silver / Gold tier, as listed on 2026-10-10:
| What you need | This actor | Official Instagram Profile Scraper | Official Instagram Post Scraper |
|---|---|---|---|
| A profile with its 12 newest posts | $1.40 / $1.27 / $1.13 / $1.00 (from 2026-10-26: $1.90 / $1.80 / $1.60 / $1.40) | $2.60 / $2.30 / $2.00 / $1.60 | |
| Every further post (caption, type, thumbnail, timestamp, plays on reels) | $0.28 / $0.27 / $0.25 / $0.24 | not available (12 posts at most) | $1.70 / $1.50 / $1.30 / $1.00 |
| A post with likes, comment count and its visible comments | $2.20 / $1.90 / $1.70 / $1.50 | $2.70 / $2.30 / $2.00 / $1.60 (post plus post details) | |
| Plays of a reel | included in the post row | ||
| Engagement rate per profile | from detailSample detail rows | ||
| Start fee | none | none | none |
Example, a Bronze (Starter plan) user: 1,000 profiles with their 50 newest posts each. Here: 1,000 profiles at $1.27 plus 38,000 further posts at $0.267 = $11.41 ($11.95 from 2026-10-26). With the official actors, the profile scraper stops at 12 posts, so the 50,000 posts come from the post scraper at $1.50 plus 1,000 profiles at $2.30 = $77.30.
What you also get here and not there: accounts that do not exist, private posts, duplicates and login walls are never billed; every failure is a skipped row you can join back to your input; all rows are flat, so a run of 5,000 posts exports to one CSV.
When the official actors are the better pick: you need the total post count of an account, business category or contact fields, music data, transcripts, video downloads, or more than about 14 comments per post. A logged-out reader cannot get those here (see Honest limits).
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
detailSamplefor an engagement rate (see How to); - up to 100 post rows (
maxPostsPerProfile, raise it to 5,000): caption, media type, product type, thumbnail, timestamp, post URL, the account that owns it, andplay_counton reels. The first 12 posts are included in the profile price. Add"postDetail": "detail"for likes, comment count, exact timestamp, video URL, usertags, location, co-authors and the visible comments.
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).
The dataset has three views in the Apify Console: Profiles, Posts and Skipped. Export any of them as JSON, CSV, Excel or XML.
Input kinds, with examples
You can mix all four in one run. Each has its own cap.
| Field | What you paste | What comes back |
|---|---|---|
usernames | natgeo, @sfmoma, https://www.instagram.com/britishmuseum/ | One profile row plus its post history |
postUrls | https://www.instagram.com/p/DdG4RIxIPyf/, or the bare shortcode DdG4RIxIPyf | One detail row per post |
hashtags | museum, #architecture | About 60 post rows per tag (see Limits) |
searchQueries | any keyword | A skipped row. Keyword search needs a login (see Limits) |
{"usernames": ["natgeo", "sfmoma", "britishmuseum"],"postUrls": ["https://www.instagram.com/p/DdG4RIxIPyf/"],"hashtags": ["museum"],"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, 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": 81761.6,"avg_comments": 278.7,"avg_video_views": null,"avg_plays": 1113332.5,"sample_size": 12,"scraped_at": "2026-10-10T03:51:56.573Z","discoveredVia": "input"}
Post row, list level (the default)
A profile timeline row carries caption, type, thumbnail and timestamp at the list price. A reel also carries play_count, the number Instagram shows on its reels grid. Likes and comment count are not on list rows since 2026-10-23 (see Honest limits): use the detail level for them.
{"type": "post","pk": "4004322549189214782","shortcode": "DeSNiSGgoI-","url": "https://www.instagram.com/p/DeSNiSGgoI-/","owner_username": "natgeo","owner_pk": "787132","media_type": "video","product_type": "clips","caption": "We could stay here a while. 🌿 ...","thumbnail_url": "https://scontent-lhr6-1.cdninstagram.com/v/t51.82787-15/8441...jpg","taken_at": "2026-10-09T19:01:07.942Z","detail_level": "list","discoveredVia": "profile:natgeo","play_count": 1067444}
Post row, engagement level
Adds view_count to a video: the legacy view count of its embed page, a smaller number than play_count. It also carries the likes and the comment count of the video, which a timeline list row no longer has. Posts that are not videos have no view count and stay list rows at the list price.
{"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}
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, and the comparison above puts them next to the official actors. 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 type | Charge event | What it covers |
|---|---|---|
| Profile row | profile-scraped | The profile row and the first 12 posts of that account, delivered as free list rows. |
Timeline post row, list level | post-scraped | A post from a profile timeline beyond the first 12. Caption, type, product type, thumbnail, timestamp, post URL, the owner, and plays on a reel. |
Hashtag row, list level | grid-post-scraped | The same fields, off a hashtag grid, plus the counts that grid printed for free. Its own event, because one grid page delivers many rows and costs less per row than a timeline page. |
Post row, engagement level | post-engagement-scraped | A video with likes, comment count and the view count of its embed page. From a timeline, a hashtag grid, or postUrls. |
Post row, detail level | post-detail-scraped | A post with likes, comment count, exact timestamp, video URL, usertags, location and the visible comments. From a timeline, a hashtag grid, or postUrls. |
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 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 rows stay list rows at the grid price, even though their grid hands over 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
skippedrow 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.
- 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.
- 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 need a login. Since 2026-10-09 Instagram answers both location queries with "Rate limit exceeded" to every logged-out reader, on every IP, and the location page carries no posts. So the actor has no locations input any more. A run that still sends one gets a free skipped row with the reason not_available_logged_out. The location field on each post row is not affected.
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. Profile timeline reels carry play_count, the number Instagram shows on the reels grid. view_count holds the video view count of the embed page at the engagement level, and the play count on hashtag rows. Plays and views are two metrics: one natgeo reel had 939,456 plays and 168,398 views on the same day.
Profile timeline list rows carry no likes or comments. Until 2026-10-22 the actor read them from the web timeline, the route Instagram's own site uses. Since 2026-10-23 Instagram answers that route with "Rate limit exceeded" to every logged-out reader, on every IP, so the actor reads the logged-out posts tab, which prints no counts. The run log says so with [ALERT][IG_FEED]. For likes and comment count use postDetail: detail (every post) or postDetail: engagement (videos, with the embed view count). The actor keeps trying the web timeline first, so the counts come back on their own if Instagram opens the route again.
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 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).
A reel pinned to the top of a grid can come without plays. Plays are read from the reels tab, newest first, as far back as the timeline goes. A pinned reel that is older than that range keeps play_count off its row.
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 10 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
Set detailSample to a number from 1 to 12. The actor fetches that many of the newest posts at the detail level and computes engagement_rate, avg_likes, avg_comments, avg_plays and sample_size from them. Each sample post is a detail row and is billed as post-detail-scraped. With the default detailSample: 0 the rate is null, because the timeline list rows carry no counts since 2026-10-23. The rate is (avg_likes + avg_comments) / follower_count, rounded to four decimals, and is null when the follower count is hidden. avg_video_views needs the embed view count, so it is filled only at postDetail: engagement.
{ "usernames": ["natgeo"], "detailSample": 3 }
Only posts since a date
{ "usernames": ["britishmuseum"], "since": "2026-01-01" }
Profile pagination stops once the posts are older than the date. Hashtag 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 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.
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 ApifyClientclient = 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
typefield:profile,post,skipped. Do not assume all rows have the same shape. - Post rows have a
detail_levelfield:list,engagementordetail.like_countandcomment_countare onengagement(videos) anddetailrows, and on hashtag rows where the grid printed them. Profile timelinelistrows have them asnull.play_countis on reels.view_countis filled atengagementlevel and on hashtag rows. - A
nullcount means "not published", never "zero". Checklike_and_view_counts_disabledbefore you report a zero. - Never retry a
skippedrow with reasonnot_found,private_no_postsornot_available_logged_out. Those do not change on a second run. Alogin_wallorrate_limitedrow is worth one retry later. - Bound spend with
maxTotalPostsandmaxPostsPerProfilebefore 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 actor | This actor | Note |
|---|---|---|
username | username | Same |
fullName | full_name | |
biography | biography | Same |
followersCount | follower_count | |
followsCount | following_count | |
postsCount | not available | Instagram does not serve the total to a logged-out reader. It is post_count: null. |
verified | is_verified | |
private | is_private | |
profilePicUrl | profile_pic_url | |
externalUrl | external_url | bio_links carries all of them, not only the first |
latestPosts[] | separate post rows | One 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[].caption | caption | |
latestPosts[].likesCount | like_count | On detail rows, and on engagement rows of videos |
latestPosts[].commentsCount | comment_count | Same |
latestPosts[].videoViewCount | view_count | Videos only, at postDetail: engagement |
latestPosts[].videoPlayCount | play_count | Reels, on every profile timeline row |
latestPosts[].isPinned | is_pinned | detail rows |
latestPosts[].paidPartnership | is_paid_partnership | |
latestPosts[].taggedUsers, coauthorProducers | usertags, co_authors | Usernames |
latestPosts[].timestamp | taken_at, taken_at_exact | taken_at is derived from the post id and is on every row. taken_at_exact is the value Instagram prints, on detail rows. |
latestPosts[].url | url | |
latestPosts[].type | media_type | image, video, carousel or unknown |
Three behaviour differences worth knowing before you migrate:
- 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.
- Failures are rows, not gaps. A handle that does not exist produces a
skippedrow you can join back to your input list. - Nothing is guessed. A value Instagram does not publish is
null, never0and never an estimate.
Input reference
| Field | Type | Default | What it does |
|---|---|---|---|
usernames | string[] | ["natgeo", "https://www.instagram.com/sfmoma/"] (prefill) | Handles, @handles or profile URLs. One profile row per account plus its posts. |
hashtags | string[] | [] | Hashtags with or without #. About 60 posts per tag, reels only. |
searchQueries | string[] | [] | Keywords. Needs a login, so each one returns a free skipped row. |
postUrls | string[] | [] | 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). |
maxPostsPerProfile | integer | 100 | 0 to 5,000. How far back to page each profile. 0 returns the profile row only. |
maxTotalPosts | integer | 10000 | Hard cap on billed post rows across all inputs (the four post events together), checked before each fetch. |
maxResultsPerHashtag | integer | 100 | Cap per hashtag. The public page ends before 100 anyway. |
postDetail | enum | list | list, engagement or detail. See the level table below. |
detailSample | integer | 0 | How many of the newest posts per profile are fetched as detail rows (billed) to compute the engagement rate. 0, the default, leaves the rate null. |
since | string (date) | none | ISO date, for example 2026-01-01. Paging stops at posts older than this. |
minViews | integer | none | Keep only posts with at least this many views. Videos and reels only. Dropped rows are not billed. |
includeComments | boolean | true | Put the visible comments (about 14) on each detail row. Never billed separately. |
proxyConfiguration | object | Apify Proxy, RESIDENTIAL | Residential is required for post pages. Your own proxy URLs are accepted. |
concurrency | integer | 4 | 1 to 8 parallel sessions. Each keeps one proxy IP and one cookie jar. |
The three post detail levels
| Level | Extra request per post | Fields it adds | Billed as |
|---|---|---|---|
list | none | Caption, media type, product type, thumbnail, taken_at, URL, owner, and play_count on reels. Hashtag rows also carry the counts their grid printed | post-scraped from a timeline, grid-post-scraped off a hashtag grid |
engagement | about 55 KB, videos only | like_count, comment_count, view_count (embed view count), video_url | post-engagement-scraped |
detail | about 88 KB | like_count, comment_count, taken_at_exact, video_url, usertags, location, co_authors, comments (about 14 visible comments). No view count. | post-detail-scraped |
Use list for the post history, plays and post metadata. Use detail when you need likes and comment counts on every post, or the comment texts. Use engagement when you need the embed view count of videos; it carries their likes and comment count too.
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.
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.
What is the difference between play_count and view_count?
play_count is the number Instagram shows on a reels grid, read from the reels tab at no extra cost. view_count is the older view count of a video's embed page, read at postDetail: engagement. For the same reel the play count is usually several times larger. Compare a field only with itself.
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 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.
Legal and responsible use
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
- Facebook Events Scraper, Past the 8-Event Limit: every public event of an organizer or a city, not the 8 Facebook shows you when logged out.
- Facebook Pages Contact Scraper: one contact row per Facebook Page with email, phone, WhatsApp, website, address, followers and rating.
Support
Use the Issues tab on this actor's Apify Store page for bugs and feature requests. Include the profile URL or the hashtag 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.