Instagram Profile Scraper: Posts and Reels
Pricing
from $10.64 / 1,000 post returneds
Instagram Profile Scraper: Posts and Reels
Instagram scraper for public profiles, no login. Give it usernames, get one row per post or reel: caption, hashtags, mentions, likes, comments, views, video length, video file, audio track, tagged accounts and location, plus an optional profile row. A profile that returns nothing is free.
Pricing
from $10.64 / 1,000 post returneds
Rating
0.0
(0)
Developer
Adrian Voss
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
2
Monthly active users
3 days ago
Last modified
Categories
Share
One row per Instagram post, without a login. Give this actor a list of public usernames and it returns a flat row per post or reel: caption, hashtags, mentions, likes, comments, views, video length, the video file, the audio track, tagged accounts and location, plus an optional profile row with followers, bio and bio link. Field names are frozen and designed to drop straight into a Sheet, a Clay table, an n8n workflow or an AI agent with no post-processing.
Who it's for
A social analyst benchmarking a brand against three competitors wants the same twenty columns for every account, every week, in one table - not four browser tabs and a screenshot. An influencer- marketing team vetting creators before a deal needs the last dozen posts with real engagement numbers attached, so they can see whether a 200k-follower account actually gets 200 likes. A content team auditing its own hashtag strategy wants every hashtag and @mention its account has used recently, counted. And an agent or workflow that already knows which accounts matter needs a single HTTP call that answers "what did they post, and how did it do" in a shape it can read.
All four get the same thing here: usernames in, one flat row per post out, priced per post returned, with a profile row that costs nothing.
Why this one
- The row shape is the product. Every field name is frozen:
shortcode,posted_at,like_count,view_count,hashtags,audio,tagged_users,carousel_children. They do not change between runs, which is what makes this safe to point a scheduled workflow or an AI agent at. The full untouched source objects come along inrawfor anything this actor does not surface as its own column. - Listing and downloading are separate jobs, and this one is listing. You get
video_url,display_urlandthumbnail_urlas direct links. Nothing is downloaded, nothing is re-hosted, and you are not billed for bytes you did not want. Fetch the media yourself, for the posts you actually chose. - Two detail levels, priced apart. "Basic" reads only each post's embed page; "Full" also reads the post's own page for the exact posting time, tagged accounts, location and collaborators, and moves about 1.7x the data. They are billed as two separate events rather than one blended price, so a "Basic" run is genuinely cheaper and not just faster.
- A cheap date filter that runs before you pay. "Only posts since" is answered from the free listing data, so posts older than your cut-off are dropped before any per-post request is made. They cost you nothing.
- Honest about the logged-out ceiling. Instagram shows a signed-out visitor 12 posts per tab and no more. This actor reads both the posts grid and the reels tab and merges them, which is the most anyone gets without an account, and it tells you in the run's summary when your limit could not be filled. See "What you can and cannot get without logging in" below.
- Never charged for a miss. A username that does not exist, a private account, a profile whose posts were all filtered out - each returns a row explaining what happened, and costs nothing.
What you get
One row per post by default (turn off "Expand rows" in Advanced to get one row per username
instead, with everything nested under posts). With "Include a profile row" on, each username
also gets one kind: "profile" row, which is never billed.
| Field | Type | Row | Description |
|---|---|---|---|
query | text | both | The username you submitted, echoed back |
found | boolean | both | true if Instagram served a readable profile |
status | text | both | OK on a match; NOT_FOUND, BLOCKED or BAD_FORMAT on a miss |
message | text | both | Plain-English reason for a miss - only present when found is false |
username | text | both | The account, normalised to a bare lowercase name |
postCount | number | both | Post rows returned and billed for this username |
truncated | boolean | both | true if more posts matched than your limit allowed |
detailCount | number | both | Post rows that got the Full-detail second request, and were charged the surcharge |
kind | text | both | profile or post |
ig_user_id | text | both | Instagram's own numeric user ID |
full_name | text | profile | Display name |
bio | text | profile | Biography text |
followers | number | profile | Exact follower count |
following | number | profile | Exact following count |
posts_count | number | profile | Total posts on the account, as Instagram displays it. Instagram itself rounds this on large accounts ("30K"), so treat it as approximate above a few thousand |
profile_pic_url | text (link) | profile | Profile picture |
is_verified | boolean | profile | Verified badge |
external_url | text (link) | profile | The link in the bio |
shortcode | text | post | Instagram's own post code |
url | text (link) | post | Permanent post link (/reel/ for reels, /p/ otherwise) |
type | text | post | reel, image or carousel |
posted_at | date | post | Posting time, UTC. Exact in "Full" detail, day-precision in "Basic" |
caption | text | post | Full caption text |
hashtags | array | post | Every hashtag, lowercased, without the # |
mentions | array | post | Every @mention, lowercased, without the @ |
like_count | number | post | Likes |
comment_count | number | post | Comments |
view_count | number | post | Views. Videos and reels only - an image has none, and the field is empty rather than zero |
duration_s | number | post | Video length in seconds |
width | number | post | Media width in pixels |
height | number | post | Media height in pixels |
display_url | text (link) | post | Full-size still |
video_url | text (link) | post | Direct .mp4 link. Empty for images |
thumbnail_url | text (link) | post | Thumbnail still |
carousel_children | array | post | One {type, display_url, video_url} per slide, for carousels |
audio | object | post | {title, artist, is_original} for reels with an attributed track |
tagged_users | array | post | Usernames tagged in the media. "Full" detail only |
location | object | post | {id, name} when the post has one. "Full" detail only |
is_paid_partnership | boolean | post | Always false - see the caveat below |
coauthors | array | post | Collaborator usernames. "Full" detail only |
detailFetched | boolean | post | true if this post's own page was read and parsed. Only these rows are charged the Full-detail surcharge |
raw | object | post | The untouched source objects: {stub, embed, post} |
scrapedAt | date | both | When this row was fetched |
Deselect any field in the Input tab's column picker to drop it from every row - query, found,
status, message and scrapedAt always stay.
What you can and cannot get without logging in
This actor never logs in, never uses a cookie or a session token, and has no input field that could accept one. That is a deliberate design choice, and it has hard consequences worth knowing before you buy:
- About 12 posts per tab. Instagram serves a signed-out visitor exactly 12 posts from the posts grid and 12 from the reels tab, and refuses every request for the next page. This actor reads both tabs and merges them, so a typical account yields somewhere between 12 and 24 unique posts. On one test account the two tabs shared 11 of 12 posts and produced 13 unique ones. No setting raises this. When your "Most posts to return" is larger than what the account exposes, the run's summary record says so explicitly rather than quietly handing you fewer rows.
is_paid_partnershipis alwaysfalse. The field does not exist anywhere in what Instagram serves a logged-out visitor - not merely absent on some posts, absent from the data entirely. It is kept as a column so the row shape stays stable for callers that expect it, but it carries no information. Do not filter on it.view_countis videos only. Images and carousels of images have no view count at all, so the field is empty for them rather than0.- No comment text. Comment counts are available; the comments themselves are behind a login.
tagged_users,locationandcoauthorsneed "Full" detail. They live only on each post's own page, which is the more expensive of the two per-post requests.- Private accounts return a miss, not an error. A private profile comes back with
found: false,status: NOT_FOUNDand a message saying so.
Pricing
- Post returned: $14 per 1,000 posts
- Full detail added: $12 per 1,000 posts
Plus a $0.00005 start fee per run. Each event above is billed independently, only when it actually returns data — misses (found:false) are never charged.
Billing is per post, in two parts:
postfires once for every post row delivered. Profile rows are never billed - onlykind: "post"rows count towardpostCount.post-detailis a surcharge that fires only in "Full" detail, and only for posts whose second request actually came back with data. A "Basic" run never fires it at all.
The surcharge exists because the two modes genuinely cost different amounts to run: "Full" reads a second page per post and moves about 1.7x the data. Charging one blended price would make every "Basic" run subsidise the "Full" ones, so the modes are priced apart instead.
A username that returns no posts (private, nonexistent, or everything filtered out by your date or media-type settings) costs nothing beyond the flat run fee. A detail request that fails or comes back empty is not charged either - you still get the row, built from what the cheaper sources gave.
How to use
- In the Apify Console. Open the actor page and click Start — the
usernamesfield is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found. - Via the API. Call it directly with a POST request — no Console needed once you have an API token:
curl "https://api.apify.com/v2/acts/accountable_eel~ig-profile-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \-X POST \-H "Content-Type: application/json" \-d '{"usernames":["instagram","natgeo"]}'
- On a schedule. Save this actor as an Apify Task with the input you want, then add a Schedule (hourly, daily, weekly) so it runs on its own — no server of your own required.
- Set "How much detail per post" to Basic if you only need engagement, media links and the audio track - it skips a whole request per post and is not charged the detail surcharge.
- Set "Only posts since" to a date when you are running the same accounts on a schedule: older posts are dropped before any per-post request, so a weekly run pays only for the week.
Input
{"usernames": ["instagram","natgeo"]}
One Instagram username per line. @name, a bare name, or a full profile link all work. No login and no cookies: this reads only what a signed-out visitor can see. Accepted formats: instagram, @natgeo, https://www.instagram.com/nasa/.
| Setting | What it does |
|---|---|
| Most posts to return per profile | Your budget control, since you pay per post. The real ceiling is about 12 to 24 per profile whatever you set - see the logged-out section above |
| Only posts since | A date like 2026-08-01. Older posts are dropped for free, before any per-post request |
| Which media to collect | All merges the posts grid and the reels tab. Reels only reads the reels tab, which is the only place some reels appear. Posts only skips the reels tab and is the cheapest |
| Include a profile row | Adds one unbilled row per username with followers, bio, bio link and verified status |
| How much detail per post | Full adds exact posting time, tagged accounts, location and collaborators, and is charged a per-post surcharge. Basic skips both |
Sample output
| query | found | status | username | postCount | truncated | detailCount | posts | kind | ig_user_id | full_name | bio | followers | following | posts_count | profile_pic_url | is_verified | external_url | shortcode | url | type | posted_at | caption | hashtags | mentions | like_count | comment_count | view_count | duration_s | width | height | display_url | video_url | thumbnail_url | carousel_children | audio | tagged_users | location | is_paid_partnership | coauthors | detailFetched | raw | scrapedAt |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| true | OK | <all rows found (full list)> | <row type (profile or post)> | <display name (profile row)> | <bio (profile row)> | <followers (profile row)> | <following (profile row)> | <total posts on the account (profile row)> | <profile picture (profile row)> | <verified (profile row)> | <bio link (profile row)> | |||||||||||||||||||||||||||||||
| <views (videos only)> | 1970-01-01T00:00:00.000Z |
A username that returns nothing comes back as a row with "found": false and is never charged.
Use it from Clay, n8n, Make, or an AI agent
This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.
curl "https://api.apify.com/v2/acts/accountable_eel~ig-profile-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \-X POST \-H "Content-Type: application/json" \-d '{"usernames":["instagram","natgeo"]}'
n8n. Add an HTTP Request node: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~ig-profile-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body Content Type JSON, JSON Body {"usernames":["instagram","natgeo"]} (swap in an expression from an earlier node for a real value).
Clay. Add an "HTTP API" column: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~ig-profile-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body {"usernames":["{{profile}}"]}, mapping the row's profile into the usernames array.
MCP. In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Instagram Scraper API: Profile Posts & Reels" — the agent will find and run this actor.
The run also writes a summary to its OUTPUT key-value record:
{requested, returned, errors[], caps[]}errors lists every username that missed and why; caps names any profile where
Instagram's logged-out limit stopped the run short of your requested count. A workflow can read
that one record instead of scanning the whole dataset to find out whether the run did what you
asked.
Tips
- Run competitor accounts as one input list rather than one run each - the profile rows are free, and one run gives you a single table to pivot on.
- Use
Posts onlywhen you are auditing captions and hashtags: reels carry the same caption data and skipping the reels tab removes a whole page load per username. - Use
Reels onlywhen you are studying short-form performance. The reels tab is the only place Instagram exposes reels that have been hidden from the main grid, and it carries play counts inline. - Keep
rawselected while you are building a workflow and deselect it once your mapping is settled - it is by far the largest field in a row. - Leave the proxy on Residential. Instagram rate-limits Apify's shared datacenter IPs on this
route, and a
REQUEST_FAILEDorBLOCKEDrow is usually that. Lowering "Max concurrency" in Advanced helps too. - Do not build a report that needs more than about two dozen posts per account. That is not a limit of this actor, it is Instagram's, and no scraper without a logged-in session gets past it.
vs. alternatives
| What it costs | What you get | Trade-off | |
|---|---|---|---|
This actor (ig-profile-scraper) | Per post returned, plus a surcharge per post in Full detail; nothing for a miss or for the profile row | One frozen, flat row per post or reel - caption, hashtags, mentions, likes, comments, views, duration, dimensions, media links, audio, tagged accounts, location - plus the raw source objects and an optional free profile row | About 12 to 24 posts per profile, because it never logs in. is_paid_partnership is always false and comment text is unavailable. It lists media, it does not download it |
| apify/instagram-scraper | About $2.70 per 1,000 posts | The Store leader by a wide margin: 4.73 stars over 590 reviews, far broader input surface (hashtags, locations, search), and deeper history | If you need hashtag or location scraping, or more posts per account than a logged-out view allows, use it. This one is narrower on purpose: profiles only, a fixed row shape aimed at agents and tables, and the raw payload passed through |
| apify/instagram-reel-scraper | About $2.60 per 1,000 reels | Reels specifically, from the same publisher | Reels only. This actor covers reels as one of three media types in the same row shape, so a mixed account does not need two runs and two schemas |
| Instagram's own Graph API | Free, with a Meta app | Official, complete, and the only way to get insights on accounts you control | It only reaches accounts you own or have been granted access to, and requires app review and a Business or Creator account. This reads public profiles you do not control, which the Graph API cannot do |
| Doing it yourself | Your time, plus the maintenance | The same data | The header set that makes these pages serve data at all, the double-encoded embed payload, the two tabs that each carry half the fields, and the fact that the pagination endpoint returns a null user logged out. That research is what this actor absorbs |
Prices for third-party tools are their published list prices as of September 2026 and are not tracked here - check the vendor before relying on the comparison.
Data & privacy
This actor reads public Instagram profiles that anyone can see without logging in. It does not log in, does not use cookies or session tokens, does not solve CAPTCHAs, and does not reveal hidden or private content. Private accounts return a miss.
The rows include usernames, display names, biographies and profile pictures, which are personal data about real people. If you process them you are the controller of that processing and need your own lawful basis for it - this actor gives you the data, not permission to use it. Do not use it to build profiles of private individuals. Not affiliated with Instagram or Meta.
Is this allowed? It collects the same public post data your browser shows a signed-out visitor, for the accounts you choose, at a modest request rate. It is built for analysis and monitoring of public accounts, not for copying Instagram. Check that your use fits Instagram's terms and your local law.
FAQ
Why do I only get about 12 posts per account?
Because nobody logged in. Instagram serves a signed-out visitor 12 posts from the profile grid and
12 from the reels tab, and answers every request for a further page with an empty user. This actor
reads both tabs and merges them, which is the maximum available without an account. Raising "Most
posts to return" will not change it; the run's OUTPUT summary tells you when this happened.
Why is is_paid_partnership always false?
The field is not present in anything Instagram serves logged out. It is kept as a column so the
row shape never changes for callers that expect it, but it carries no information and should not
be filtered on.
Why is view_count empty on some rows?
Those rows are images or image carousels, which have no view count. Videos and reels have one.
Empty is deliberate - reporting 0 would look like a real measurement.
What is the difference between Basic and Full detail?
Basic reads one page per post (the embed) and gives caption, hashtags, mentions, likes, comments,
views, video length, dimensions, the media links and the audio track, with the posting date to the
day. Full reads a second page per post and adds the exact posting time, tagged accounts, location
and collaborators, at about 1.7x the data transfer and a per-post surcharge. The detailFetched
column tells you, row by row, which posts actually got the second request.
Does it download the videos and images?
No. You get direct links (video_url, display_url, thumbnail_url, and per-slide links for
carousels) and fetch what you actually want yourself. Listing and downloading are kept as separate
jobs so you are never billed for bytes you did not ask for.
Can I get comments? Comment counts, yes. Comment text, no - it is behind a login, and this actor does not log in.
Do I need a proxy? Residential is the default and you should leave it on. Instagram served these pages happily to a plain request from an ordinary connection during the build probe, but it answered Apify's shared datacenter IPs with HTTP 429 on the first live run: one username was rescued by session rotation on its third attempt and the other exhausted its retries. The same call through Residential returned every row with no errors. That is why the price is set against residential data transfer rather than the cheaper datacenter rate.
Can an AI agent call this directly?
Yes. It is registered on the Apify MCP server, and the field names are frozen specifically so an
agent's mapping does not break between runs. The OUTPUT summary record gives an agent a single
place to check whether the run was complete.
Related actors
- TikTok Profile Lookup - follower, video and engagement stats for a TikTok handle, same no-login approach.
- Threads Profile Lookup - the equivalent profile lookup for Meta's Threads.
- Pinterest Search Scraper - one row per pin for a keyword search, billed per row the same way.