Instagram API Scraper
Pricing
from $4.99 / 1,000 results
Instagram API Scraper
⚡ Lightning-fast Instagram scraper. Extract posts, reels, comments, profile details, hashtag posts, and search results from any public Instagram URL or username — no login, no cookies, no headless browser.
Pricing
from $4.99 / 1,000 results
Rating
0.0
(0)
Developer
Scraper Engine
Maintained by CommunityActor stats
0
Bookmarked
34
Total users
8
Monthly active users
22 days ago
Last modified
Categories
Share
Instagram API Scraper — Posts, Reels, Likes and Captions
Instagram API Scraper extracts public Instagram profile feeds through Instagram's own JSON API — no login, no cookies, no headless browser. Each post or reel comes back as a typed row: caption, hashtags, mentions, like count, comment count, timestamp, real carousel children and every image size variant Instagram sent. Paste usernames or profile URLs, pick posts, reels, mentions or profile details, and rows stream into the dataset as each page lands.
⚠️ Read this before your first run. Instagram walls large parts of its API behind a session cookie. Profile posts, reels, mentions and partial profile details work logged-out. Comments, stories, single-post and single-reel URLs, hashtag feeds, location feeds and search do not — those targets return an uncharged accounting row carrying a typed errorReason instead of a charged empty row. Nothing is silently substituted, and you are not billed for a wall you cannot pass.
What is Instagram API Scraper?
Instagram API Scraper is an Apify Actor that calls Instagram's public web API endpoints directly and normalizes the responses into stable JSON rows. One run covers one mode: a profile's post feed, a profile's reels feed, @mentions mined from captions, or profile identity fields.
No Instagram account, login, cookie or session is used. Every value it returns is already served to an anonymous visitor by Instagram's own endpoints.
It is built for social-media analysts tracking creator output, growth and agency teams auditing competitor feeds, and developers piping public post data into a dashboard, warehouse or AI agent.
What Instagram data is publicly available to scrape?
Instagram serves a public profile's whole media feed to signed-out clients — captions, media, like counts and comment counts included. What it does not serve without a session cookie is conversation, ephemeral content, discovery surfaces, and the profile-info aggregate.
| Data Category | Signed-out visitor | Requires an Instagram login |
|---|---|---|
| A public profile's post feed — captions, media, likes, comment counts | ✅ Public | — |
| A public profile's reels feed | ✅ Public | — |
| Owner username, full name, verification badge, profile picture | ✅ Public | — |
Hashtags and @mentions written inside captions | ✅ Public | — |
| Carousel children and every image size variant | ✅ Public | — |
| Comments and replies on a post or reel | ❌ | Login — the media-info endpoint redirects to /accounts/login/ |
| Stories and highlights | ❌ | Login — the story tray endpoints redirect to /accounts/login/ |
| Single post / reel detail by shortcode URL | ❌ | Login |
| Hashtag feeds, location feeds and Instagram search | ❌ | Login |
| Follower, following and post counts, bio, external URL, category | ❌ | No keyless endpoint remains — see below |
| Private accounts, follower lists, direct messages | ❌ | Login plus an approved follow |
Instagram API Scraper only returns publicly visible data — what any signed-out visitor sees. Nothing behind a login wall.
⚠️ Follower counts and bios are not available to this Actor. The Actor's build-time probe (2026-07-19) found Instagram's profile-info surface /api/v1/users/web_profile_info/ answering HTTP 429 from every network exit, and HTTP 400 even when replayed inside a live logged-in browser. resultsType: "details" therefore returns followersCount, followsCount, postsCount, biography, externalUrl, category and isBusinessAccount as null, lists them in unavailableFields, and explains why in unavailableReason — rather than shipping zeros and empty strings that read as real data.
What data can I extract with Instagram API Scraper?
The Actor writes five row shapes into one dataset. Every row carries a section field naming its shape, and the Actor ships a dataset view per shape so you can read, sort or export them separately.
section | Row shape | Charged |
|---|---|---|
posts | One media row per post — photo, video or carousel | ✅ row_result |
reels | One media row per reel, same shape, type forced to Video | ✅ row_result |
profiles | One profile identity row per target (resultsType: "details") | ✅ row_result |
mentions | One row per @mention found in a caption | ✅ row_result |
accounting | One diagnostic row per blocked, walled, private or empty target | ❌ Uncharged |
search | Search result rows — structurally supported, login-walled in practice | ✅ row_result |
Post and reel media fields
Posts and reels share one schema, so a feed run and a reels run join on the same keys.
| Field Name | Description |
|---|---|
id | Instagram's numeric media id, as a string |
type | Photo, Video or Sidecar (carousel). Reels rows are always Video |
productType | Instagram's own product label for the media, e.g. clips, feed, carousel_container |
shortCode | Media shortcode — the segment in instagram.com/p/<shortCode>/ |
url | Canonical post URL built from shortCode |
caption | Full caption text as written by the account owner |
hashtags | Array of hashtag words parsed out of the caption, without the # |
mentions | Array of usernames parsed out of the caption, without the @ |
likesCount | Like count, or null when the owner has hidden counts |
likeCountsHidden | true when Instagram flagged like and view counts as hidden by the owner |
commentsCount | Comment count as reported on the media, or null |
isCommentsDisabled | true/false when Instagram sends a usable signal, null when it sends none |
firstComment | Text of the first preview comment, or null when no comment reaches a logged-out client |
latestComments | Array of preview comment objects, or null — see the nested shapes below |
timestamp | UTC ISO timestamp the media was posted |
captionCreatedAt | UTC ISO timestamp the caption itself was written, or null |
captionIsEdited | true/false when Instagram carries the flag, null when it does not |
displayUrl | Highest-priority image URL Instagram returned for the media |
images | Flat array of image URLs — the media's own, plus one per carousel child |
allImageVariants | Every image size variant in the payload, each with url, width, height |
imageVariantCount | Number of entries in allImageVariants |
dimensionsHeight | Original media height in pixels, or null |
dimensionsWidth | Original media width in pixels, or null |
alt | Instagram's accessibility caption for the media, or null |
childPosts | Array of carousel child objects — empty array for single-media posts |
childPostsCount | Number of entries in childPosts |
ownerUsername | Account handle that posted the media |
ownerFullName | Display name on the posting account |
ownerId | Numeric account id, as a string |
ownerIsVerified | true/false when the payload carries the flag, null when it does not |
fbCrossPostId | Facebook cross-post id (fbid) when Instagram sends one, otherwise null |
inputUrl | The exact directUrls entry this row came from |
scrapedAt | UTC ISO timestamp the row was built |
section | Row shape tag — posts or reels |
parentData | Source profile object. Present only when addParentData is true — omitted, not nulled |
Media state and honesty rules
Three of these fields exist specifically so that a missing value never reads as a real one.
likesCount is null — never 0 — when likeCountsHidden is true, because Instagram sends no trustworthy count in that state. isCommentsDisabled is null when Instagram sends no usable signal; the Actor reads comments_disabled, then is_social_ufi_disabled, then inverts comment_likes_enabled, and stops there rather than manufacturing a false. firstComment and latestComments are null rather than "" and [], because an empty array would read as "checked, and there are none" when the truth is "not reachable logged-out".
allImageVariants is the field that costs nothing extra. Instagram already ships every size variant of a media in the same response — typically 10 to 14 of them — and most scrapers keep only the first. This Actor keeps all of them with real pixel dimensions, so you can pick a thumbnail or a full-resolution frame without a second request.
Nested objects and arrays
childPosts[] — one object per carousel child, with id, type (Image or Video), shortCode, url, dimensionsHeight, dimensionsWidth, displayUrl, allImageVariants, images, alt, ownerId, childPosts (always an empty array), and caption, hashtags, mentions, likesCount, timestamp, commentsCount, firstComment, latestComments set to null. Those last eight are genuinely not exposed per child, so they are declared unavailable rather than faked as 0 and "". Instagram sends no shortcode for some children, in which case shortCode and url are null rather than a link pointing at instagram.com/p//.
allImageVariants[] — url, width, height per variant.
latestComments[] — id, text, ownerUsername, ownerProfilePicUrl, timestamp, repliesCount, replies, likesCount, and an owner object. The nested owner object keeps Instagram's original snake_case naming — username, full_name, profile_pic_url, profile_pic_id, is_verified, is_private, is_mentionable, latest_reel_media, id — while its parent object is camelCase. That inconsistency is upstream and is preserved so your mapping matches what actually arrives. On the primary feed surface this array is null; it only fills when the Actor falls back to Instagram's Relay timeline query and that response happens to carry preview comments.
parentData — name, type, url for the source profile. Added to post and reel rows only, and only when addParentData is true. It does not attach to profile-detail rows or mention rows.
Profile detail fields
Returned by resultsType: "details", one row per profile target.
| Field Name | Description |
|---|---|
id | Numeric account id, as a string |
username | Account handle |
fullName | Display name, or null |
isVerified | Verification flag, or null when the payload omits it |
isPrivate | Private-account flag, or null when the payload omits it |
profilePicUrl | Standard-resolution avatar URL |
profilePicUrlHD | Highest-resolution avatar URL available in the payload |
profileUrl | Canonical profile URL |
detailsSource | Which surface the row was built from — post_feed_user_object |
unavailableFields | Array naming every field with no keyless source on this run |
unavailableReason | Plain-language explanation of why those fields are null |
biography | Always null — no keyless source |
externalUrl | Always null — no keyless source |
followersCount | Always null — no keyless source |
followsCount | Always null — no keyless source |
postsCount | Always null — no keyless source |
isBusinessAccount | Always null — no keyless source |
category | Always null — no keyless source |
inputUrl | The directUrls entry this row came from |
scrapedAt | UTC ISO timestamp the row was built |
section | Row shape tag — profiles |
Mention fields
Returned by resultsType: "mentions": type (always mention), mentionedUsername, postShortCode, postUrl, ownerUsername, timestamp, inputUrl, scrapedAt, section. Mentions are mined from the captions of the target profile's own posts — this is "who this account tags", not "who tags this account".
Uncharged accounting fields
Every accounting row carries type (always accounting), target, targetKind, resultsType, errorReason, message, itemsReturned, inputUrl, scrapedAt and section (always accounting). These rows are pushed without a charged event name, so a wall, a block, a private account or a typo shows up in your dataset without being billed as a row_result.
errorReason is a fixed vocabulary of eleven values:
errorReason | What happened |
|---|---|
COMMENTS_LOGIN_WALLED | No comment data reaches a logged-out client on any surface |
STORIES_LOGIN_WALLED | Stories need an authenticated session |
POST_DETAIL_LOGIN_WALLED | A single post or reel URL was given; the media-info surface is walled |
HASHTAG_LOGIN_WALLED | A hashtag URL was given; the tag feed is walled |
LOCATION_LOGIN_WALLED | A location URL was given; the location feed is walled |
SEARCH_LOGIN_WALLED | search was set; Instagram's search endpoint redirects to the login page |
EMPTY_FEED_PRIVATE_OR_RESTRICTED | HTTP 200 with zero items — private, restricted, age-gated or no public posts |
PROFILE_ID_UNRESOLVED | No numeric user id could be resolved from any live surface |
UNSUPPORTED_TARGET_FOR_MODE | The target kind does not fit the selected resultsType, or could not be parsed |
FETCH_FAILED | Every live surface failed for this target after retries |
NO_MENTIONS_IN_CAPTIONS | Captions were scanned successfully and contained no @mention |
To read only billed data rows, filter on the absence of errorReason:
clean = [r for r in rows if r.get("errorReason") is None]
Search fields
If Instagram reopens /api/v1/web/search/topsearch/ to signed-out clients, the Actor already shapes the response: user rows carry type, id, username, full_name, is_verified, is_private, profile_pic_url, follower_count; hashtag rows carry type, id, name, media_count, search_result_subtitle, url; place rows carry type, id, name, subtitle, lat, lng, url. Every search row also gets scrapedAt and section. The original snake_case keys are kept for backward compatibility and camelCase aliases are added alongside them — fullName, isVerified, isPrivate, profilePicUrl, followerCount, mediaCount, searchResultSubtitle — so the dataset contract stays consistent with the other sections. In practice this path returns SEARCH_LOGIN_WALLED today.
🤖 Add-on: Need additional Instagram data?
Four sibling Actors extend this base engine rather than duplicating it. Instagram API Scraper With Reel Views & Audio Tracks adds reel view counts, video URLs and audio-track attribution. Instagram API Scraper By Likes, Keywords & Date Range applies engagement, keyword and date filters during the run instead of after it. Instagram API Scraper & Brand Collaboration Partners adds tagged accounts, co-authors and sponsorship partners. Instagram API Scraper: Engagement Rate Per Follower adds follower-denominated engagement rate.
How does Instagram API Scraper differ from the official Instagram Graph API?
Meta's Instagram Graph API is public and documented, but its scope is accounts you control: it reads Instagram Business and Creator accounts that you or your app's users have authorized through OAuth, after Meta app review. It is not a route to arbitrary third-party public profiles. Instagram API Scraper reads the public web endpoints instead, which is why it accepts any public username as input — and also why the walled surfaces listed above stay walled.
| Feature | Instagram Graph API | Instagram API Scraper |
|---|---|---|
| Reads a third-party public profile you do not own | ❌ Authorized accounts only | ✅ Any public profile |
| Meta app review and OAuth flow | ✅ Required | ❌ Not used |
| Instagram Business or Creator account required | ✅ Required | ❌ Not required |
| Comments, stories and insights | ✅ For authorized accounts | ❌ Login-walled — uncharged accounting row |
| Follower, following and post counts | ✅ For authorized accounts | ❌ null, declared in unavailableFields |
| Output shape | Meta's documented Graph API schema | One normalized row per media item, stable across runs |
| Request budget | Published per-app limits — check Meta's docs | 12 items per request, 200 pages, 2400 items per target |
Check Meta's current published Instagram Graph API documentation for the exact permissions, review requirements and rate limits before choosing between them — those terms change and are not reproduced here.
Use the Graph API when you own or manage the accounts, need comments and insights, and want a supported contract. Use Instagram API Scraper for competitor and creator research across accounts you have no authorization over.
How to use Instagram API Scraper
Instagram API Scraper runs on Apify. Start it from the Apify Console or call it through the Apify API — the only credential involved is your Apify token.
- Open Instagram API Scraper on Apify and click Try for free
- Paste one or more public profile URLs or bare usernames into Instagram URLs or usernames (
directUrls) — this is the only required input - Pick What to scrape (
resultsType):posts,reels,mentionsordetails. Leave it onpostsfor a profile feed - Set Max results per URL (
resultsLimit) — 1 to 2400 per target - Optionally set Only items newer than (
onlyPostsNewerThan) to an absolute date or a relative window - Leave Proxy configuration off unless you want to force a tier — the Actor runs direct and escalates by itself
- Click Start, then export the dataset as JSON, CSV, Excel or XML, or read it through the Apify API
Match the target to the mode. Profile URLs and bare usernames work for all four working modes. Single post URLs, reel URLs, hashtag URLs and location URLs are login-walled by Instagram — give the owner's profile instead, and the profile feed returns the post.
How to scale to bulk profile extraction
directUrls is a list, so one run can cover as many profiles as you need. Both plain strings and { "url": "..." } objects are accepted, so a list produced by another tool usually feeds in unchanged, and @handle, handle and a full profile URL all resolve to the same target.
resultsLimit is per target, not a run total: ten profiles at resultsLimit: 100 is up to 1000 rows. The engine only opens as many pages as your limit needs — ceil(limit / 12) + 2, capped at 200 pages — so a small limit does not pay for deep pagination. Targets are processed one after another with a short randomised pause between them. For recurring collection, put the same input on an Apify schedule and diff likesCount and commentsCount per shortCode between runs.
What can you do with Instagram post and reel data?
- 📈 A social media analyst benchmarking a competitor pulls 200 posts per account and ranks them by
likesCountagainstcommentsCountto separate reach from conversation, usingproductTypeto compare reels against static feed posts. - 🏷️ A brand strategist auditing partnerships runs
resultsType: "mentions"across a creator list and countsmentionedUsernameperownerUsernameto see which brands each creator actually tags, and how often. - 🖼️ A content ops team building an asset library reads
allImageVariantsandchildPoststo pull the exact resolution they need per carousel frame, without a second request per image. - 🔍 A growth marketer tracking posting cadence re-runs the same profiles weekly with
onlyPostsNewerThan: "7 days"and groupstimestampby weekday to find when a competitor actually publishes. - 🤖 An AI engineer building a social-listening agent indexes
caption,hashtags,mentionsandownerUsernameinto a vector store, so the agent can answer "what themes has this account posted about this month" against live feed data rather than a stale export. - 🧾 A data engineer running the same job nightly filters on
errorReasonto build an exception queue — private accounts, walled targets and failed fetches all arrive as typed, uncharged rows instead of silent gaps.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.
How does Instagram API Scraper handle rate limits and blocking?
There is no headless browser and no CAPTCHA solver in this Actor — it does not solve challenges, and it does not claim to. What it does is keep a plain HTTP session credible and escalate its network path only when Instagram pushes back.
Every run starts on a direct connection with no proxy at all, because Instagram's live endpoints answer the Apify container directly. Requests carry a rotating Linux Chrome or Firefox user agent — matched to the container's actual operating system rather than advertising a Windows client from a Linux host — plus Instagram's own web headers. The public app id and CSRF token are harvested at runtime from a real page load, with the cookie jar read before the HTML. That token matters: the reels endpoint is a POST and answers HTTP 403 without it.
When Instagram returns 401, 403, 407, 429, 451 or 503, the Actor climbs a fixed ladder: direct → Apify datacenter → Apify residential, up to three retries on the residential rung, then sticky residential for the rest of the run. Residential endpoints are pinned to sticky sessions because Instagram binds its cookies to the exit IP that issued them, and the session is rebuilt behind the new exit after an escalation for the same reason. On 429 and 503 the Actor also backs off with jitter before retrying. Pages within a target are spaced by a randomised delay, and targets by another.
If a target still cannot be fetched after three failed pages, the Actor writes an uncharged accounting row with errorReason: "FETCH_FAILED" and moves on. The run continues; nothing is fabricated and nothing is billed for the failure. The final log line names the proxy tier the run ended on.
⬇️ Input
Nine parameters, read straight from the Actor's input schema. Only directUrls is required.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
directUrls | Yes | array | Instagram profile URLs or bare usernames, one per line. Plain strings and { "url": "..." } objects are both accepted. Single post, reel, hashtag and location URLs are accepted as input but are login-walled by Instagram and return an uncharged accounting row. | ["https://www.instagram.com/humansofny/", "natgeo"] |
resultsType | No | string | What to return per target. One of posts, comments, details, mentions, reels, stories. Default "posts". comments and stories are login-walled and always return an uncharged accounting row. | "posts" |
resultsLimit | No | integer | Maximum items per target. Minimum 1, maximum 2400 — the engine's real ceiling of 200 pages at 12 items per page. Default 10. | 100 |
onlyPostsNewerThan | No | string | Only return items posted on or after this cutoff. Absolute YYYY-MM-DD, or a relative window such as 7 days, 2 months, 3 years. Empty means no filter. All times UTC. | "30 days" |
addParentData | No | boolean | Attach a parentData object with the source profile to each post or reel row. Default false. | false |
search | No | string | Search query. Instagram's search endpoint redirects logged-out clients to the login page, so this returns an uncharged accounting row rather than results. Kept for input compatibility. Default "". | "" |
searchType | No | string | What the search query targets. One of user, hashtag, place. Default "hashtag". Only read when search is set. | "hashtag" |
searchLimit | No | integer | Number of search results to return. Minimum 1, maximum 250. Default 1. Only read when search is set. | 1 |
proxyConfiguration | No | object | Apify Proxy settings. Default {"useApifyProxy": false} — the Actor runs direct and escalates only when Instagram pushes back. Set a group to force a starting tier; a country code localises the residential rung. | {"useApifyProxy": false} |
Parameters that behave differently from what their names suggest — stated plainly:
search,searchTypeandsearchLimitcannot return results today. Instagram'stopsearchendpoint redirects signed-out clients to the login page. Settingsearchproduces one uncharged accounting row witherrorReason: "SEARCH_LOGIN_WALLED". The three inputs exist for compatibility with other Instagram Actors' input shape.resultsType: "comments"andresultsType: "stories"are valid enum values that always produce an uncharged accounting row, never data. They stay in the enum so a job configured for another Actor does not fail validation — and so the run tells you why it returned nothing instead of finishing green and empty.resultsLimithas two defaults. The schema default is10, which Apify Console fills in for you. If the key is absent entirely — for example an API call that omits it — the engine falls back to30.onlyPostsNewerThanis validated by a schema pattern that accepts onlyYYYY-MM-DDandN day|week|month|year(s). The engine's parser also understands hours and full ISO timestamps, but the schema regex rejects them before the run starts. Relative months are treated as 30 days and years as 365 days.onlyPostsNewerThanstops paginating at the first item older than the cutoff. Instagram feeds are reverse-chronological, so this is normally what you want — but a pinned post that predates your cutoff sits at the top of the feed and will end that target early. Leave the filter empty if a profile pins old content.addParentDataattachesparentDatato post and reel rows only. Profile-detail rows and mention rows are unaffected.
Example input
{"directUrls": ["https://www.instagram.com/natgeo/","https://www.instagram.com/humansofny/","nasa"],"resultsType": "posts","resultsLimit": 100,"onlyPostsNewerThan": "30 days","addParentData": true,"search": "","searchType": "hashtag","searchLimit": 1,"proxyConfiguration": {"useApifyProxy": false}}
⬆️ Output
Typed, normalized JSON with a consistent shape per section across runs. Rows are pushed live as each page finishes, so the dataset fills while the run is still going. Export as JSON, CSV, Excel or XML from the Storage tab, or read the dataset through the Apify API. Seven dataset views ship with the Actor — Posts and reels, Posts overview, Reels, Profile details, Mentions, Run accounting and Search — so each row shape gets a readable table with no transformation on your side.
Absent values are null, not fabricated zeros or empty strings. The one field omitted rather than nulled is parentData, which appears only when addParentData is true.
Example output
A carousel post row from a posts run with addParentData: true:
{"id": "3641182904472519038","type": "Sidecar","shortCode": "DK7pQzvNXa1","caption": "Fifteen years after the eruption, the forest is coming back. Photographs by @carstenpeter. #volcano #reforestation","hashtags": ["volcano", "reforestation"],"mentions": ["carstenpeter"],"url": "https://www.instagram.com/p/DK7pQzvNXa1/","commentsCount": 412,"firstComment": null,"latestComments": null,"dimensionsHeight": 1350,"dimensionsWidth": 1080,"displayUrl": "https://scontent.cdninstagram.com/v/t51.2885-15/512094871_1080x1350_n.jpg","images": ["https://scontent.cdninstagram.com/v/t51.2885-15/512094871_1080x1350_n.jpg","https://scontent.cdninstagram.com/v/t51.2885-15/512094872_1080x1350_n.jpg"],"alt": "Photo by National Geographic on July 18, 2026.","likesCount": 128744,"timestamp": "2026-07-18T14:02:11.000Z","childPosts": [{"id": "3641182899812340011","type": "Image","caption": null,"hashtags": null,"mentions": null,"url": "https://www.instagram.com/p/DK7pQzvNXa2/","commentsCount": null,"firstComment": null,"latestComments": null,"dimensionsHeight": 1350,"dimensionsWidth": 1080,"displayUrl": "https://scontent.cdninstagram.com/v/t51.2885-15/512094872_1080x1350_n.jpg","allImageVariants": [{ "url": "https://scontent.cdninstagram.com/v/t51.2885-15/512094872_1080x1350_n.jpg", "width": 1080, "height": 1350 },{ "url": "https://scontent.cdninstagram.com/v/t51.2885-15/512094872_750x938_n.jpg", "width": 750, "height": 938 }],"images": ["https://scontent.cdninstagram.com/v/t51.2885-15/512094872_1080x1350_n.jpg"],"alt": null,"likesCount": null,"timestamp": null,"childPosts": [],"ownerId": "787132","shortCode": "DK7pQzvNXa2"}],"ownerFullName": "National Geographic","ownerUsername": "natgeo","ownerId": "787132","isCommentsDisabled": false,"inputUrl": "https://www.instagram.com/natgeo/","productType": "carousel_container","ownerIsVerified": true,"fbCrossPostId": "1284470912345678","captionCreatedAt": "2026-07-18T14:02:11.000Z","captionIsEdited": false,"likeCountsHidden": false,"allImageVariants": [{ "url": "https://scontent.cdninstagram.com/v/t51.2885-15/512094871_1080x1350_n.jpg", "width": 1080, "height": 1350 },{ "url": "https://scontent.cdninstagram.com/v/t51.2885-15/512094871_750x938_n.jpg", "width": 750, "height": 938 },{ "url": "https://scontent.cdninstagram.com/v/t51.2885-15/512094871_640x800_n.jpg", "width": 640, "height": 800 },{ "url": "https://scontent.cdninstagram.com/v/t51.2885-15/512094871_320x400_n.jpg", "width": 320, "height": 400 }],"imageVariantCount": 4,"childPostsCount": 1,"scrapedAt": "2026-07-25T09:41:07.000Z","parentData": {"name": "natgeo","type": "profile","url": "https://www.instagram.com/natgeo/"},"section": "posts"}
A profile row from a details run, showing the declared-unavailable contract:
{"id": "787132","username": "natgeo","fullName": "National Geographic","biography": null,"externalUrl": null,"followersCount": null,"followsCount": null,"postsCount": null,"isVerified": true,"isPrivate": false,"isBusinessAccount": null,"profilePicUrl": "https://scontent.cdninstagram.com/v/t51.2885-19/44884218_150x150_n.jpg","profilePicUrlHD": "https://scontent.cdninstagram.com/v/t51.2885-19/44884218_1080x1080_n.jpg","category": null,"inputUrl": "https://www.instagram.com/natgeo/","profileUrl": "https://www.instagram.com/natgeo/","detailsSource": "post_feed_user_object","unavailableFields": ["biography","externalUrl","followersCount","followsCount","postsCount","isBusinessAccount","category"],"unavailableReason": "Instagram retired /api/v1/users/web_profile_info/ (HTTP 429 from every exit; HTTP 400 even inside a live logged-in browser). Follower/following/post counts, bio, external URL, category and the business flag have no keyless HTTP source and are reported as null rather than guessed.","scrapedAt": "2026-07-25T09:41:12.000Z","section": "profiles"}
A mention row, then an uncharged accounting row for a private account:
{"type": "mention","mentionedUsername": "carstenpeter","postShortCode": "DK7pQzvNXa1","postUrl": "https://www.instagram.com/p/DK7pQzvNXa1/","ownerUsername": "natgeo","timestamp": "2026-07-18T14:02:11.000Z","inputUrl": "https://www.instagram.com/natgeo/","scrapedAt": "2026-07-25T09:41:15.000Z","section": "mentions"}
{"type": "accounting","target": "https://www.instagram.com/some_private_account/","targetKind": "profile","resultsType": "posts","errorReason": "EMPTY_FEED_PRIVATE_OR_RESTRICTED","message": "Instagram returned HTTP 200 with 0 items for this profile. The account is private, restricted, age-gated or has no public posts.","itemsReturned": 0,"inputUrl": "https://www.instagram.com/some_private_account/","scrapedAt": "2026-07-25T09:41:19.000Z","section": "accounting"}
How does it work?
Instagram serves its own web app from JSON endpoints, and this Actor calls those endpoints directly instead of rendering pages. It loads a real Instagram page once to harvest the public app id and the CSRF token, then reads a profile's feed from /api/v1/feed/user/{username}/username/ at 12 items per request, and a profile's reels from /api/v1/clips/user/. If the primary feed surface fails, it resolves the numeric user id from a live surface and retries through Instagram's Relay timeline GraphQL query, mapping that response onto the same item shape so there is only ever one output contract.
Requests start direct and escalate to Apify datacenter, then Apify residential proxies, only when Instagram returns a blocking status. There is no browser and no CAPTCHA solving.
Because the Actor parses structured API responses rather than rendered markup, an Instagram front-end redesign does not affect it and your field names stay put. Only publicly visible data is collected — no account, cookie or session is used, and a private account returns a typed accounting row rather than data.
Integrations
Instagram API Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset — Apify schedules and webhooks, the Python and Node.js clients, and every no-code platform with an Apify connector.
Calling Instagram API Scraper from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/instagram-api-scraper").call(run_input={"directUrls": ["https://www.instagram.com/natgeo/", "humansofny"],"resultsType": "posts","resultsLimit": 100,"onlyPostsNewerThan": "30 days",})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row.get("errorReason"):print("skipped:", row["target"], row["errorReason"])continueprint(row["ownerUsername"], row["shortCode"], row["likesCount"], row["commentsCount"])
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Branch on section, or on the presence of errorReason, to route each row shape to the right handler.
No-code tools (n8n, Make, LangChain)
In n8n, use the Apify node — or an HTTP Request node pointed at the Actor's run endpoint with your token — and pass the same JSON input shown above; a Switch node on section then splits posts, profiles, mentions and accounting rows into separate branches. In Make, the Apify module supports run-and-wait, so a weekly competitor sweep can feed a Google Sheets or Airtable step directly. In LangChain, wrap the run endpoint as a tool and hand the returned rows to the agent unchanged — they are already typed JSON, so no output parser is needed. Apify schedules and webhooks cover recurrence and downstream triggers.
Is it legal to scrape Instagram posts and profiles?
Scraping publicly accessible content is broadly treated as permissible where no authentication is bypassed, and Instagram API Scraper reads only what a signed-out visitor is served — no account, no cookie, no session. Private accounts are not accessible: they return HTTP 200 with zero items, which the Actor reports as EMPTY_FEED_PRIVATE_OR_RESTRICTED rather than data.
This output contains personal data. These fields identify or describe real people and fall under the GDPR in the EU and UK and the CCPA/CPRA in California: ownerUsername, ownerFullName, ownerId, ownerIsVerified, caption (user-written text that can name and describe people), mentions and mentionedUsername, username, fullName, profilePicUrl, profilePicUrlHD, profileUrl, and every latestComments[] entry including ownerUsername, ownerProfilePicUrl and the nested owner object. biography, followersCount and followsCount remain in the output contract but are always null in this Actor.
You therefore need a lawful basis before you store or reuse these rows — consent, or a legitimate interest you have actually assessed and documented — and data minimisation should be applied in practice: keep only the fields your use case needs, drop caption and latestComments when you are only counting engagement, use Storage → Export dataset → Omit fields to strip identity fields at export, and set a retention period instead of keeping snapshots indefinitely. Aggregate analysis of engagement metrics carries far less exposure than building a searchable profile database, and data subjects keep their access and erasure rights over whatever you hold.
Consult legal counsel if your use case involves bulk storage of personal data, cross-border transfer, or any form of profiling.
❓ Frequently asked questions
What Instagram fields does Instagram API Scraper return?
The five most used are caption, likesCount, commentsCount, timestamp and ownerUsername. A post or reel row carries 34 keys, plus parentData when you enable it, and the allImageVariants and childPosts arrays add nested detail underneath. Profile, mention and accounting rows have their own shapes — see the data fields section above for all of them.
Does Instagram API Scraper require an Instagram account or login?
No. No Instagram account, no login, no cookie, no session token. The Actor harvests only Instagram's public app id and a CSRF token from a normal page load, exactly as a signed-out browser does. The one credential you need is your Apify token. This is also the reason comments, stories and search are unavailable — those surfaces require a session, and the Actor reports that instead of faking it.
How many posts or reels can I extract in one run?
Up to 2400 per target, and resultsLimit is per target rather than a run total — so ten profiles at 2400 is up to 24,000 rows in one run. The 2400 ceiling is the engine's, not Instagram's: pagination runs 12 items per request across a maximum of 200 pages, and the input schema's maximum is set to that same number so the advertised limit is one the code can actually reach. Apify rejects a larger value before the run starts rather than silently clamping it.
What happens if a profile is private, or a target is login-walled?
You get an uncharged accounting row instead of a charged empty one. A private, restricted or age-gated account returns HTTP 200 with zero items, which becomes errorReason: "EMPTY_FEED_PRIVATE_OR_RESTRICTED". A single post URL, reel URL, hashtag URL, location URL, resultsType: "comments" or resultsType: "stories" returns its own typed reason — POST_DETAIL_LOGIN_WALLED, HASHTAG_LOGIN_WALLED, LOCATION_LOGIN_WALLED, COMMENTS_LOGIN_WALLED or STORIES_LOGIN_WALLED — with a message naming exactly which endpoint refused. To get a specific post's data, scrape its owner's profile: profile feeds are fully available. In code, filter clean rows with row.get("errorReason") is None.
Why are followersCount and biography always null?
Because no keyless Instagram endpoint serves them any more. The surface that carried follower, following and post counts, the bio, the external URL, the category and the business flag was /api/v1/users/web_profile_info/, and the Actor's build-time probe found it returning HTTP 429 from every network exit and HTTP 400 even when replayed inside a live logged-in browser session. Rather than substituting zeros, resultsType: "details" returns those seven fields as null, names them in unavailableFields, and explains the cause in unavailableReason. Everything the post feed's user object does carry — id, username, full name, verification, private flag, and both profile picture resolutions — is returned normally.
Can I scrape multiple Instagram profiles at once?
Yes. directUrls is a list. Paste as many profile URLs or bare usernames as you need, one per line, or use Bulk edit. Plain strings and { "url": "..." } objects are both accepted, and resultsLimit applies to each target independently. Targets are processed sequentially with a short randomised pause between them.
Does Instagram API Scraper work with Claude, ChatGPT and other AI agent tools?
Yes. It is callable as a standard HTTP-triggered Apify Actor run, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. Rows are tagged with section, and failures carry errorReason, so an agent can branch on either without inspecting the payload shape.
How does Instagram API Scraper compare to other Instagram scrapers?
Checked on the Apify Store on 25 July 2026, the three most-used Instagram Actors are all published by Apify. apify/instagram-scraper is the broadest: its listing documents posts, reels, comments, mentions, profile details, hashtag volume and place metadata, plus search-based discovery, and it documents returning likesCount: -1 for creators who hide their like count. apify/instagram-profile-scraper documents follower and following counts, bio, external URLs, related profiles and the latest 12 posts, with a paid "About profile" add-on for join and verification dates. apify/instagram-reel-scraper documents reel-specific fields its listing calls out — shares, views, plays, transcripts, music info and downloadable video URLs.
Those three cover surfaces this Actor deliberately does not: comments, stories, hashtag feeds, search, follower counts and video URLs are outside its scope, and it says so in typed error codes rather than empty rows. Where it differs is what it does with the response it already has. It keeps every image size variant (allImageVariants, imageVariantCount) instead of the first one only, emits real carousel children with per-child pixel dimensions and variants instead of placeholder objects, returns likesCount as null alongside a likeCountsHidden flag instead of a sentinel number, exposes captionCreatedAt, captionIsEdited, productType, ownerIsVerified and fbCrossPostId from the same payload, and pushes uncharged accounting rows with an eleven-value errorReason vocabulary for every target it could not serve. Uncharged diagnostic rows and per-variant image arrays are not documented on those three listings.
Does Instagram API Scraper return data in a format LLMs can use directly?
Yes. Every row is typed, normalized JSON with the same field names on every run, and nested values are plain JSON objects and arrays rather than encoded strings. No HTML parsing, no selectors. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool.
What happens when Instagram changes its layout or anti-bot system?
The scraper is maintained, and because it reads Instagram's JSON endpoints rather than rendered markup, a front-end redesign generally does not affect it. It also carries a second path by design: if the primary feed endpoint fails, it falls back to Instagram's Relay timeline query and maps that response onto the same item shape. If a surface goes away entirely, the run writes an uncharged accounting row rather than degrading into fabricated or silently empty data. Your field names and types stay the same on your end.
Can I use Instagram API Scraper without managing proxies or browser infrastructure?
Yes, and by default you use no proxy at all. The Actor starts on a direct connection, and only if Instagram returns a blocking status does it climb to an Apify datacenter proxy and then to Apify residential with sticky sessions, rebuilding its session behind each new exit. You never create a proxy account, rotate an IP or run a browser. Setting a group in proxyConfiguration forces a starting tier, and a country code localises the residential rung.
Which Instagram fields work best for AI training data and RAG indexing?
For RAG indexing: caption is by far the highest-information text field per row and chunks cleanly, with hashtags and mentions as ready-made metadata filters and ownerUsername plus url as citation anchors. For training data and time-series work: likesCount, commentsCount, timestamp, type, productType, childPostsCount and imageVariantCount are the most structurally consistent fields across records. All values arrive as typed primitives, arrays or plain nested objects, so no normalization pass is needed — but note that missing values are null, so decide up front whether your pipeline imputes or drops them.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| Instagram Profile Scraper | Profile-level fields for public Instagram accounts |
| Instagram Hashtag Scraper | Instagram content discovered from a hashtag |
| Instagram Mentions Scraper | Public Instagram posts that tag a target profile |
| Instagram Related Person Scraper | Related and suggested accounts around an Instagram profile |
| Instagram Comment Leads Scraper | Commenter identities from Instagram posts |
| Threads User Posts Scraper | Posts from a Threads account, which shares the Instagram username |
💬 Your feedback
Found a bug, or need a field that is in Instagram's payload but not yet in the output? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON and the target username are the fastest to reproduce and fix — and if a surface this README calls login-walled starts answering signed-out requests again, that is worth reporting too.