Instagram Profile Scraper – Posts & Followers avatar

Instagram Profile Scraper – Posts & Followers

Pricing

$8.00 / 1,000 profile reads

Go to Apify Store
Instagram Profile Scraper – Posts & Followers

Instagram Profile Scraper – Posts & Followers

Scrape any public Instagram profile for its latest posts — caption, likes, comments, images, video URLs and carousels — plus the account record with follower count and bio. No login, no cookies, no API key. Built for polling many accounts often: 12 posts per profile. Unofficial.

Pricing

$8.00 / 1,000 profile reads

Rating

0.0

(0)

Developer

Simple Actor

Simple Actor

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

3 days ago

Last modified

Share

Scrape any public Instagram profile for what it has posted lately — caption, likes, comments, images, video files, carousels — together with the full account record: followers, following, bio, bio links, category, business contact details and verification. 12 latest posts per profile — or 24 with the video tab switched on — newest first, 57 fields each. Built for social media monitoring, influencer research and competitor tracking across many accounts at once.

$8 per 1,000 profiles read — Apify platform usage, residential proxy and the clean-IP route this Actor carries on its own side all included, nothing else to pay. The charge is per profile and does not depend on how many posts come back, so a full read and a check that finds one new post cost the same. A profile that cannot be read is never charged, and does not fail the run: it comes back as a row saying why. No login, no cookies, no API key, no account of yours involved. There is no session to keep alive and nothing of yours to get restricted.

Unofficial. Not affiliated with, endorsed by, or sponsored by Instagram or Meta.

Read this before you start: 12 posts per profile from the main grid, and 12 more if you turn on the video tab. That is Instagram's limit for a logged-out reader, not a setting. This is built to check many accounts often — if you need one account's back catalogue, this is the wrong tool.

Pick your row shape up front. The default is one row per post, with the account record repeated on each — right for collecting posts, wasteful for watching accounts. Set outputFormat: "profile" and each account is a single row instead, at the same price.

Features

  • Latest posts per public profile — up to 12 from the grid, 24 with the video tab, newest first, 57 fields each.
  • Full account record — followers, following, total posts, bio, bio links, category, business email and phone, verification, profile pictures.
  • Every media type handled — single images, videos with a direct file URL and play count, and carousels with every slide individually.
  • Two output shapes at the same price — one row per post, or one row per account with its posts nested.
  • Caption parsed — hashtags and mentions extracted, plus tagged accounts, collaborators, location, music credits on reels and alt text.
  • Collaborations attributed correctlyownerUsername comes from the post, so a collab posted to two grids is credited to its real owner, with coauthors listing the rest.
  • A second timeline for free — the long-form/IGTV tab arrives in the same request and shares no posts with the grid.
  • Related accounts — up to fifty accounts Instagram suggests alongside this one, for discovery.
  • Incremental pollingonlyPostsNewerThan takes 20 hours, 3 days or an ISO date, and refuses rather than misleads if the window reaches too far back.
  • Three doors to every account. Instagram's profile endpoint first; the accounts it refuses with its own internal error are read from its post feed; and when Instagram turns a logged-out read away altogether — which it now does from most addresses — the profile page itself is read, over a clean-IP route the Actor carries on its own side. Each row says which door it came through in isProfilePartial and isPostPartial, so a null is never mistaken for a zero.
  • Runs stay green. A missing, private or mistyped account is a row, not a failed run.

Input

{
"startUrls": [{ "url": "https://www.instagram.com/nasa/" }], // profiles as links
"usernames": ["natgeo", "@who"], // or as usernames — both lists are read as one
"outputFormat": "posts", // "posts" = row per post, "profile" = row per account
"maxPosts": 12, // latest posts per account (max 12, or 24 with the video tab)
"includeVideoTab": false, // add the long-form/IGTV timeline — doubles output, same cost
"includeRelatedProfiles": false, // attach the accounts Instagram suggests alongside
"onlyPostsNewerThan": "3 days", // optional window: "20 hours", "3 days", "2026-08-01"
"includeRaw": false, // attach Instagram's untouched post object under `raw`
"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

The shortest useful input is a single profile:

{ "startUrls": [{ "url": "https://www.instagram.com/nasa/" }] }

If you have usernames rather than links, put them in usernames instead — both lists are read as one:

{
"startUrls": [{ "url": "https://www.instagram.com/nasa/" }],
"usernames": ["natgeo", "@who"]
}
FieldTypeDefaultWhat it does
startUrlsarrayProfiles to read, as links.
usernamesarrayProfiles to read, as usernames. Combined with startUrls.
outputFormatstringpostsposts gives one row per post; profile gives one row per account with its posts under posts.
maxPostsintegerallHow many latest posts per account. Ceiling is 12, or 24 with the video tab.
includeVideoTabbooleanfalseAlso read the long-form/IGTV timeline — roughly doubles the output, same request.
includeRelatedProfilesbooleanfalseAttach the accounts Instagram suggests alongside this one.
onlyPostsNewerThanstringKeep only posts after a point: "20 hours", "3 days", or 2026-08-01.
includeRawbooleanfalseAttach Instagram's untouched post object under raw.
proxyobjectResidentialLeave this alone unless you know why you are changing it.

Output

One dataset item per post — or one per account, if you would rather have the profile stated once with its posts attached (both shapes):

  • The post — caption, permalink, publish time, type (image, video, carousel)
  • Engagement — likes, comments, and play counts on video
  • Media — full-size image URL and every smaller rendition; for video, a direct file URL; for a carousel, every slide with its own image or video
  • Context — hashtags and mentions read from the caption, tagged accounts, collaborators, location, music credits on reels, alt text
  • The account — bio, bio links, followers, following, total posts, category, business contact details, verification, profile pictures
{
"type": "Sidecar",
"shortCode": "DcJ2mQxFabc",
"url": "https://www.instagram.com/p/DcJ2mQxFabc/",
"caption": "Hold the dots and scroll to switch between…",
"likesCount": 216896,
"commentsCount": 716,
"timestamp": "2026-08-14T21:48:00.000Z",
"childPostCount": 20,
"hashtags": ["NASA", "space", "astronomy"],
"ownerUsername": "nasa",
"owner": { "followersCount": 104409230, "isVerified": true }
}

Two shapes: one row per post, or one row per account

The item above is the default: one row per post, with the account record on each of them. It is the right shape for collecting posts — pipe it into a table and every row stands alone.

If you are looking at accounts rather than collecting posts, set outputFormat to profile and each account becomes a single row: the profile at the top level, and its latest posts in short form under posts.

{
"username": "westvillenyc",
"fullName": "Westville",
"url": "https://www.instagram.com/westvillenyc/",
"biography": "Neighborhood restaurants…",
"profilePicUrlHd": "https://…",
"followersCount": 48213,
"postsCount": 3241,
"categoryName": "Restaurant",
"isVerified": false,
"postsReturned": 5,
"posts": [
{
"id": "3972256348138218143",
"type": "Sidecar",
"shortCode": "DcgShpLkaKf",
"url": "https://www.instagram.com/p/DcgShpLkaKf/",
"caption": "Summer specials are officially here…",
"hashtags": [],
"mentions": ["goldencalibear"],
"timestamp": "2026-08-25T14:19:51.000Z",
"timestampUnix": 1787667591,
"likesCount": 1247,
"commentsCount": 31,
"isVideo": true,
"videoUrl": "https://…",
"videoViewCount": 4925,
"displayUrl": "https://…",
"alt": null,
"locationName": "Westville",
"isPinned": false,
"childPostCount": 2,
"ownerUsername": "westvillenyc",
"source": "grid",
"isPostPartial": false
}
]
}

That is the complete post shape in this format — all 22 fields, not an excerpt. In particular displayUrl is here: the post's full-size image is on every post in this format, so you do not need the row-per-post format to get the pictures. What the short form drops is the long tail: every smaller rendition of each image (images), the blur-up placeholder, the per-slide childPosts, tagged-account coordinates, music credits, and the streaming-manifest fields. If you need any of those, use the default format.

Same charge either way — it is one profile read, so this costs no less and no more. What it saves is size: measured on a real five-post read, 13.9 KB against 101 KB, because the profile is stated once instead of once per post.

Two timelines for one request

Instagram serves a second timeline alongside the grid — the account's long-form and IGTV videos — in the same request, sharing no posts with the grid. Set includeVideoTab to double what a run returns at no extra cost. Every item says which one it came from in source (grid or videos).

It is off by default because that tab reaches years further back, and folding it in silently would change what "latest" means.

Instagram also ships up to fifty suggested accounts with every profile. Set includeRelatedProfiles to get them under owner.relatedProfiles, each with a username, name, ID, verification and link — a ready-made way to widen a watchlist.

How to use

From Apify Console

  1. Open the Actor and click Try for free / Start.
  2. Put profile links into Instagram profiles, or a column of bare usernames into Usernames — both lists are read together.
  3. Decide the row shape in What a row is: one row per post for collecting posts, one row per account for watching accounts. Same price either way.
  4. Turn on Also read the video tab to roughly double what comes back at no extra cost.
  5. Leave the proxy on Residential — Instagram refuses a repeated address.
  6. Click Start, then open the Dataset tab and export as JSON, CSV or Excel.

To monitor accounts, schedule the run and set Only posts newer than to a window shorter than the gap between runs.

From the API

curl -s "https://api.apify.com/v2/acts/simple.actor~instagram-profile-posts/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"usernames":["nasa"]}'

Watching a list of accounts, with the JavaScript client:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actor/instagram-profile-posts').call({
usernames: ['nasa', 'natgeo', 'who'],
outputFormat: 'profile', // one row per account
includeVideoTab: true, // 24 posts instead of 12, same charge
onlyPostsNewerThan: '24 hours',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
// Treat any row with an `error` field as the failure signal — the run stays green.

Use cases

  • Monitor competitor and brand accounts — poll a list of profiles on a schedule with onlyPostsNewerThan and see new posts, captions and engagement as they appear.
  • Influencer research and vetting — pull follower counts, verification, category, business contact details and recent engagement across a shortlist in one run.
  • Engagement benchmarking — compare likes and comments per post across accounts in the same niche, loaded straight into a warehouse.
  • Hashtag and campaign tracking — hashtags and mentions come parsed out of every caption, so you can measure which tags an account actually uses.
  • Media archiving — collect full-size images, video files and every carousel slide for a set of accounts (download promptly — the URLs expire).
  • Account discovery — switch on related accounts to expand a seed list into the neighbours Instagram itself suggests.

Usage notes

Instagram allows one address about nineteen requests before refusing it, and the refusal lasts far longer than the message claims — measured still in force 33 minutes after "please wait a few minutes". So every attempt goes out from a different IP, which is what residential proxy provides.

The default is already set correctly. If you switch to datacenter, the pool is too small to rotate through and runs will come back rate_limited.

A post on a profile is not always by that profile

Instagram collaborations appear on both accounts' grids but are owned by one of them. ownerUsername is read from the post, so a collaboration is attributed to its real owner rather than to the profile you asked for — and coauthors lists the credited accounts.

What an empty result means

An empty dataset means the account was read and has posted nothing in the window you asked for. It never means "we could not look".

An account that cannot be read is reported as its own row with error and errorDescription, so one bad account never costs you the rest:

errorMeaningRun status
not_foundNo such accountSucceeds
privatePosts are visible only to approved followers, so a login would be neededSucceeds
bad_inputThe entry was never a usable username or profile URLSucceeds
window_too_wideonlyPostsNewerThan reaches further back than this run can seeSucceeds
instagram_errorInstagram returned an internal error for this account on both routes — see belowFails only if every account did
rate_limitedInstagram refused a logged-out read from every address tried, through every door including the clean-IP route — rare; a re-run almost always passesFails only if every account did
read_failedInstagram did not serve a readable response after several attemptsFails only if every account did

A missing, private or mistyped account does not fail the run. Those are answers about the account you asked for, not a fault, and a run reporting them finishes as SUCCEEDED with the reason in a row. The run's status message says how many accounts were read and how many answered with an error.

So check the rows, not the run status. If you are scheduling this, treat any row with an error field as the failure signal — a green run can still contain accounts that were not read. The one case that does fail a run is every account in it coming back unreachable, which means Instagram or the proxy could not be reached at all rather than anything about your accounts.

An empty dataset still means exactly one thing — read, nothing new — because every failure leaves a row.

Accounts Instagram's own endpoint refuses

Some business accounts make Instagram's profile endpoint return an internal error about a deleted asset — reproducible per account, a fault on Instagram's side rather than the account's. Those accounts are read anyway, through a second Instagram endpoint that does serve them.

The posts come back complete. The account record does not: that second endpoint carries the username, display name, picture and verification, and nothing else. The rest of the profile fields read null rather than being guessed at, and the row says so in isProfilePartial: true — so a null follower count is "could not be read", never zero. Check that flag before charting anything from the profile fields.

instagram_error is now reported only when both routes fail for an account.

When Instagram turns a logged-out read away

Instagram increasingly answers its own endpoints with a login requirement for readers who are not signed in — from most addresses, for most accounts, including public ones. When both endpoints refuse an account, it is still read: from the profile page itself, first over your proxy and then over a clean-IP route the Actor carries on its own side, at no extra charge.

What comes back through that door is honest about itself. The profile carries followers, following, post count, bio, links, verification and privacy — isProfilePartial: true, because it is not the endpoint's full record. The latest 12 posts carry their id, link, type, caption, alt text, one image, a carousel's slide count and an exact timestamp, and say isPostPartial: true, because likesCount, commentsCount, the extra image renditions, videoUrl, tagged users, location and music read null — Instagram does not put those on the page for a logged-out reader. A null there is "not on the page", never zero. If your use depends on engagement counts, filter on isPostPartial and treat those posts as counts-unknown.

What it costs

$3 per 1,000 profiles read. One profile is one charge, so asking for five profiles in one run costs five. There is no per-post fee and no per-run fee.

Two consequences worth knowing:

  • Turn on the video tab. It doubles what you get — 24 posts instead of 12 — for exactly the same charge, because it arrives in the same request.
  • Take all the posts. Fewer posts cost you no less; the price is for reading the profile.

Because the charge is per profile, what you pay per post depends on how many come back:

Posts returnedWorks out at
24 — grid plus video tab$0.13 per 1,000 posts
12 — grid only$0.25 per 1,000 posts
1 — a poll that found one new post$3.00 per 1,000 posts

If you are polling with onlyPostsNewerThan and most checks come back empty, budget by profiles checked, not posts collected — that is what you are billed on. A profile that cannot be read is never charged, so a list containing dead or private accounts costs only for the live ones.

onlyPostsNewerThan will refuse rather than mislead

A run sees an account's latest posts and no further back. If your window reaches past them, posts inside it were never fetched — and returning what was found would read as "this is everything since then". So the account is reported with window_too_wide instead, telling you how far back it could actually see. A pinned post does not count towards that reach, since it can be years old.

Limits

12 posts per profile from the grid, 24 with the video tab — Instagram's numbers, not settings.

Media URLs expire. Image and video URLs are signed by Instagram and die within hours. Download them promptly; the post's url is the durable one.

No comment text. This endpoint carries comment counts but no comments. firstComment and latestComments are present and empty so existing code does not break on their absence.

Private accounts cannot be read. They need an approved follower's login, which this deliberately does not have.

FAQ

Is scraping Instagram legal? This Actor reads only posts that are publicly visible to anyone without logging in — it does not log in, use cookies, or reach private accounts, stories or direct messages. Meta's Terms of Service restrict automated collection from its properties, so check the platform's ToS and your own obligations, particularly around personal data, before using it.

Do I need an Instagram login, cookies or an API key? No. There is no session to keep alive and nothing of yours to get restricted.

Can I scrape a profile's entire post history? No. Instagram serves a logged-out reader 12 posts from the grid, plus 12 more from the video tab — that is Instagram's limit, not a setting. This Actor is built to check many accounts often; for one account's back catalogue it is the wrong tool.

Does it support pagination? There is no next page to request. includeVideoTab is the only way to get more posts per profile, and it roughly doubles the output in the same request at no extra charge.

Can it read private accounts? No. Those need an approved follower's login, which this deliberately does not have. A private account comes back as a row with error: "private" and the run still succeeds.

Why do I need a residential proxy? Instagram allows one address about nineteen requests before refusing it, and the refusal was measured still in force 33 minutes later. Residential proxy sends every attempt from a different IP. Switching to datacenter returns rate_limited error rows.

Why is the follower count null on some accounts? Some business accounts make Instagram's own profile endpoint return an internal error, so they are read through a second endpoint that carries the posts but only part of the profile. Those rows set isProfilePartial: true — a null there means "could not be read", never zero.

Does it return comment text? No. This endpoint carries comment counts but no comments. firstComment and latestComments are present and empty so existing code does not break.

Why did my run succeed when an account was not read? By design: a missing, private or mistyped account is an answer about that account, not a fault, so it comes back as a row with an error field. Treat those rows as the failure signal rather than the run status. Only every account failing fails the run.

Note

This reads publicly visible posts only — the same ones anyone can see without logging in. It does not log in, does not use cookies, and cannot reach private accounts, stories, direct messages, or anything else behind a login.