Instagram Profile Posts Scraper — Instagram User Posts, Reels avatar

Instagram Profile Posts Scraper — Instagram User Posts, Reels

Pricing

from $0.60 / 1,000 posts

Go to Apify Store
Instagram Profile Posts Scraper — Instagram User Posts, Reels

Instagram Profile Posts Scraper — Instagram User Posts, Reels

Instagram user posts: posts and reels from any public Instagram profile — captions, hashtags, engagement, media links — by handle or URL, no login. From $0.60 per 1,000 posts. Plus $0.003 per profile that delivers posts. Set 30, get 30. Nothing undelivered is charged.

Pricing

from $0.60 / 1,000 posts

Rating

0.0

(0)

Developer

Steadyfetch Team

Steadyfetch Team

Maintained by Community

Actor stats

0

Bookmarked

44

Total users

15

Monthly active users

2 hours ago

Last modified

Share

Your result limit is the law. Set it to 30 and you get 30 — not 31, not 240 because the page size said so. Every post and reel from any public Instagram profile, no login and no cookies, and a post we could not deliver is never charged.

Using an AI agent? Pin this actor in Apify's MCP server with one link: https://mcp.apify.com?tools=steadyfetch/instagram-profile-posts

  • Actor id: steadyfetch/instagram-profile-posts
  • Input: { "profiles": ["nasa"] } — the one field you have to set.
  • Cap the bill: set maxTotalChargeUsd on the run (a run option, not Actor input), e.g. 0.50 — the run stops when it reaches it.

Paste a handle. Get the posts — the newest page or the full archive back to the feed's start, in one run.

Just want to see it work? Click Start with nothing set and the run collects the 5 newest posts of one public profile (bbcnews), charged like any run — about a cent — so the first thing you see is real rows in the exact schema. Press it again and the same posts come back uncharged. Set only the filters — a post type, a date, a limit — and no profiles, and the same sample runs under your settings, charged like any run; a limit larger than the sample's own is capped at it. Add your own handles for your own run.

Instagram Profile Posts Scraper input form in the Apify console: the Instagram profiles list, with the limits, filters, chaining and monitoring sections below it

Running the same profiles again? A post already delivered to your account is never charged a second time — it is skipped, with the count on the status line, or handed back uncharged if you ask for it (a scoped API token needs key-value store Read, Write and Create permission for that check to run — see Never pay for the same post twice).

Unofficial. Not affiliated with, endorsed by, or sponsored by Instagram or Meta Platforms, Inc. This actor reads publicly available profile pages; Instagram is a trademark of Meta Platforms, Inc.


Output

One row per post, one row per profile, and every row carries charged and status — so your invoice reconciles from the dataset itself. Real rows below, taken from a verified run against nasa (captions trimmed with … for the page):

Dataset table of a real Instagram Profile Posts Scraper run: the 5 newest nasa posts with type, format, taken-at, caption, likes, comments, plays and video duration

shortCodetypetakenAtcaptionlikeCountcommentCountplayCountvideoDuration
DchLnq8E21Ncarousel2026-08-26T21:30:18ZHow it started 👉 how it's going. On Aug. 30, our Nancy Grace Roman Space Telescope will lift off to space…19,990140
Dcg_WJpGXfhimage2026-08-26T19:43:06ZA shining star. NASA honors the memory of Dolly Parton, who passed away on Aug. 25, 2026…127,200308
Dcea3BiPTBmvideo2026-08-25T19:55:51ZTeamwork makes the dream work! High fives and cheers rang out both in space and on Earth during today's spacewalk…45,0783673,430,67841.1

Newest first means newest by date. Instagram's own grid leads with the posts the owner pinned to the top, so "the latest 3" on a public API is usually three pinned posts and none of the three newest. Rows here are ordered by takenAt, so your limit buys the most recent posts — pinned posts still ship, in their real date position, flagged with isPinned so you can filter them.

The first row above in full, exactly as it lands in the dataset (caption and signed URL trimmed with … for the page):

{
"postId": "3972507450993765709",
"shortCode": "DchLnq8E21N",
"url": "https://www.instagram.com/p/DchLnq8E21N/",
"type": "carousel",
"productType": "carousel_container",
"caption": "How it started 👉 how it's going\n\nOn Aug. 30, our Nancy Grace Roman Space Telescope will lift off to space, where it will change the way we view the cosmos…",
"hashtags": ["nasa", "roman", "launch"],
"mentions": ["nasakennedy"],
"takenAt": "2026-08-26T21:30:18.000Z",
"takenAtTimestamp": 1787779818,
"likeCount": 19990,
"commentCount": 140,
"playCount": null,
"videoDuration": null,
"displayUrl": "https://instagram.fruh4-3.fna.fbcdn.net/v/t51.82787-15/788528368_…",
"videoUrl": null,
"videoUrlExpiresAt": null,
"carouselCount": 2,
"dimensionsWidth": 1440,
"dimensionsHeight": 1920,
"ownerUsername": "nasa",
"ownerFullName": "NASA",
"ownerIsVerified": true,
"taggedUsers": [],
"coauthors": [],
"isPinned": false,
"isSponsored": false,
"postSource": "feed",
"profileHandle": "nasa",
"charged": true,
"status": "delivered",
"statusReason": null
}

And one profile row per handle you asked about — uncharged, always:

{
"profileHandle": "nasa",
"profileUrl": "https://www.instagram.com/nasa/",
"fullName": "NASA",
"biography": "Making the seemingly impossible, possible. ✨",
"externalUrl": "https://www.nasa.gov/",
"followers": 104427542,
"following": 92,
"postCount": 4893,
"isVerified": true,
"isPrivate": false,
"isBusiness": true,
"categoryName": "Government Agencies",
"postsReturned": 30,
"postsFiltered": 0,
"charged": false,
"status": "profile",
"statusReason": "Profile row for \"nasa\" — 30 posts returned. This row itself carries no result fee."
}

Every field is always present (explicit null over silent omission), so a row from the profile route and a row from the depth route are the same shape.

With a watchlist name set, every post row also carries the two fields the list adds —

"isNew": true
for a post the list had not delivered before, "isNew": false for one it had, and "firstSeenAt", the moment it was first delivered (this run's clock for a new post, the original sighting for an old one). Without a watchlist both read null.

Agent / API paste-block

Actor: steadyfetch/instagram-profile-posts (Apify Store)
Does: Instagram handle -> every post and reel that public profile has published,
or (resultsType: "tagged") the posts OTHER accounts published that tag it.
One JSON row per post + one uncharged profile row per handle.
No Instagram login, no cookies.
Run: POST https://api.apify.com/v2/acts/steadyfetch~instagram-profile-posts/run-sync-get-dataset-items?token=APIFY_TOKEN
Input:
{
"profiles": ["nasa", "https://www.instagram.com/bbcnews/"],
"resultsType": "posts", // posts = the profile's own grid | tagged = posts that tag it
"resultsLimit": 30, // HARD cap per profile, newest BY DATE (up to 5,000)
"maxItems": 1000, // HARD cap for the whole run (up to 100,000)
"maxRunSeconds": 1800, // clean stop before the platform timeout; 30 s to 1 h, deep archives fit at 1800-3600
"mediaType": "any", // any | image | video | carousel
"postedAfter": "2026-01-01",
"postedBefore": "2026-08-01",
"datasetId": "<dataset id of a run that emits handles>",
"datasetItems": [ { "username": "nasa" } ],
"resumeCursor": { }, // from a previous run's summary row: continues it exactly
"watchlistId": "acme-brands", // optional: a separate named list per profile set
"newPostsOnly": false, // ON: fail-closed — stop rather than risk charging a post you have
"includeSeen": false // ON: hand back posts you already have (repeat: true), uncharged
}
Nulls: every optional field above accepts null, meaning "use the default" — so a template
that renders one body per run can send null for whatever it left unset.
"profiles" is the one field that needs a real value.
Output post rows: postId, shortCode, url, type, productType, caption, hashtags[],
mentions[], takenAt, takenAtTimestamp, likeCount, commentCount, playCount,
videoDuration, displayUrl, videoUrl, videoUrlExpiresAt, carouselCount,
dimensionsWidth, dimensionsHeight, ownerUsername, ownerFullName,
ownerIsVerified, taggedUsers[], coauthors[], isPinned, isSponsored,
postSource, profileHandle, charged, status, statusReason
on resultsType "tagged": ownerUsername is ANOTHER account, profileHandle is
still the handle you asked for, and postSource reads "tagged"
profile rows (uncharged): profileHandle, profileUrl, fullName, biography,
externalUrl, followers, following, postCount, isVerified, isPrivate,
isBusiness, categoryName, postsReturned, postsFiltered
one final summary row: delivered, profilesRequested, stoppedBy, resumeCursor
Pricing pay per event, platform usage INCLUDED, nothing charged for starting a run:
post $0.0024 -> $0.0006 per delivered post (falls with your Apify plan tier)
profile-lookup $0.003 flat per profile that delivers >= 1 post
Charged only on delivery — private accounts, wrong handles, empty profiles,
filtered-out posts and Instagram's own walls cost $0, lookup included.
Cap any run with maxTotalChargeUsd (minimum $0.05).
MCP: reachable through Apify's MCP server; ask for "instagram profile posts".

Input

Handles (nasa), or profile URLs (https://www.instagram.com/nasa/). Capital letters, a leading @ and a trailing slash are all fine. You can also chain a Dataset ID from a previous run and this actor will find the profiles in it.

Coming from another Instagram actor? Instagram links or handles pasted into Instagram links or handles (startUrls, or urls) are sorted and served here too — plain links, or the [{"url": "..."}] shape — at the same limit and the same price as the ones you type into Instagram profiles, and a handle in both lists is collected once. A post or reel link names one post rather than a profile, so its row is uncharged and points you at our reel transcript actor. This actor returns posts, not transcripts, so there is no language setting.

Optional: what to collect (the profile's own posts, or the posts that tag it — see Posts that TAG a profile), max posts per profile, max posts for the whole run, post type, posted-after and posted-before dates, max run seconds.

Every one of those optional fields accepts null, meaning "use the default" — so a template or an agent that renders one request body per run can send null for whatever it left unset instead of building a different body each time. profiles is the one field that needs a real value.

Leave the input empty and the run is the default sample: the 5 newest posts of bbcnews, charged like any run, so you see the exact schema on real data. The status line says it was the sample and how to run your own handles. Set only filters and no Instagram profiles and that sample runs under them — your settings are kept, everything you did not set takes the sample's own value, and one uncharged row names which was which.

Posts that TAG a profile

Set What to collect (resultsType) to tagged and the run collects the posts other accounts published that tag your handles — Instagram's Tagged tab — instead of the profiles' own grid. Everything else is the same: same handles, same Max posts per profile, same filters, same charged unit, same price. One charged unit is one post delivered, on either door.

{ "profiles": ["gopro"], "resultsType": "tagged", "resultsLimit": 50 }

What changes is whose posts come back, and the rows say so:

columnon the profile's own postson the posts that tag it
profileHandle ("Profile")the profile you asked forthe profile you asked for
ownerUsername ("Posted by")the same profile (or a co-author)another account, every time
postSource ("Read from")feed or profile-pagetagged

Every other column is the one you already get: caption, hashtags, mentions, taggedUsers, likes, comments, plays, media links, location, isSponsored. A tagged post that is a reel chains into the reel transcript actor exactly like any other.

Two things this door will not pretend to know. Nothing Instagram publishes counts the posts that tag a profile — the profile's own postCount is its own grid and says nothing about this tab — and the tab can be curated, so an account really can have nothing in it. When a grid comes back unreadable this actor therefore says that, uncharged, and asks you to re-run: it will not tell you a tab is empty when it cannot prove it. And a profile that could not be read at all is uncharged on this door exactly as it is on the other.

How often this changes: as often as other people post about you, which for most accounts is daily and for a busy brand is hourly — measured 2026-09-14, gopro's newest tagged post was minutes old and a page of 21 spanned about twenty minutes, while a quiet account's newest was years old. A daily schedule catches a busy handle; weekly is plenty for a quiet one. Repeat memory applies here too, so a re-run delivers and charges only what is new.

Never pay for the same post twice

Every run remembers the posts it delivered for your account — in a key-value store called ig-posts-watch-account in your own Apify account, under every identity the post has (its short code and its numeric id). Run the same profiles again, at any depth, and the posts you already have are skipped before they take a row slot: not delivered again, not charged, and not counted against your limits — the run keeps reading until it has filled Max posts per profile with posts you do not already have. The status line says how many (4 already in your account, skipped, not charged) and OUTPUT.skippedSeen holds the number. The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (Write is what lets the run remember what it delivered; without it every run pays again) — or Actor runs set to Full access — under Settings → API & Integrations; without it the run says so on its status line and in one uncharged row, and it cannot skip what you already have.

Want one complete dataset per run instead of only what changed? Turn on Include posts you already have (includeSeen: true) and those posts come back anyway — marked repeat: true with firstSeenAt and firstSeenRunId naming the run that first delivered them,

charged: false
. You get the whole picture and pay only for what is new.

Delete that store to forget everything; entries older than 90 days count as new again. If the store cannot be read on some run, the run still runs — it delivers and charges as usual and the status line says the repeat check was unavailable, so you know a repeat may have been charged that once.

Watch profiles on a schedule

Following a profile is rarely a one-off: the question is usually what did they post since last week? A watchlist keeps a separate list per profile set, on top of the account-wide memory above.

  1. Put the profiles in Instagram profiles (a chained dataset works too).
  2. Give the run a Watchlist name, e.g. acme-brands, and turn on New posts only.
  3. Save the input as a Task, put the Task on an Apify Schedule (daily fits most profiles), and add an integration or a webhook on run succeeded — a Google Sheet, Slack, n8n, Make, or your own endpoint.

Put every profile in ONE scheduled run, not one run each. It finishes sooner — a run starts once and reads profiles side by side — and it costs you exactly the same: the post fee is charged per delivered post and the profile fee once per profile, whichever way the profiles are split across runs. A run that finds nothing new charges neither.

{
"profiles": ["nasa", "natgeo"],
"resultsLimit": 30,
"watchlistId": "acme-brands",
"newPostsOnly": true
}

Each scheduled run then delivers only the posts that are new, each with isNew: true and its firstSeenAt. Posts you already have are skipped before they take a row — not delivered again and not charged — and the run's status line says exactly what happened, for example: 4 already in your account, skipped, not charged. The same numbers sit in the run's OUTPUT record under watchlist, newItems, skippedSeen and repeatMemory. With New posts only on, a post you already have still holds its place in the window the run reads, which is what keeps a daily monitoring run reading the newest Max posts per profile rather than walking into the archive to replace it. Leave New posts only off and the opposite holds: a post you already have costs you nothing at all, so the run keeps reading until it has filled your limit with posts you lack.

Some details worth knowing:

  • The list lives in your account, in a named key-value store called
    ig-posts-watch-<your name>
    (one record, SEEN). Open it in Storage → Key-value stores to see every post the actor has delivered under that name, or delete the record to start the list over. This actor runs with limited permissions and touches nothing else in your account.
  • Only delivered posts go on the list. A profile Instagram would not serve this minute, a private account or a wrong handle leaves nothing behind, so the next run tries it again.
  • What "new" compares against. The actor reads each profile's most recent posts (up to Max posts per profile) and checks those against the list — so a daily run with the default 30 catches anything posted since, and a profile that posts more than that a day just needs a higher number.
  • Leave New posts only off and posts you already have are still never charged twice — the account memory above does that on its own. Turn Include posts you already have on to get them back in every run's dataset, uncharged.
  • New posts only makes the rail fail-closed. With it on, a run that can read neither your watchlist nor your account's memory ships one uncharged row and stops, rather than risk charging you for posts you may already have. A named watchlist it cannot open stops the run the same way, before any spend. A scoped API token without key-value store Read, Write and Create permission is the usual reason both fail at once — the run then names the permissions to grant instead of asking you to re-run.
  • The listing itself is one request per profile page whatever it holds; what the filter saves is the rows, the charges and your limits. One post, one sighting, however it arrives.

Deep archives: ask once, get the archive

Set a date window or a deep result limit and the run walks the profile's feed to wherever your ask ends — verified live on a 718-post profile: the full feed, back to its first post from December 2011, in a single ~9-minute run. A 1,000-post archive takes about 13 minutes; leave "Max run seconds" at 1800, or raise it to 3600 for the biggest profiles.

A date window that sits far back is read to a bounded depth rather than to the end of the archive — about forty pages past what your own "Max posts per profile" needs — and if the window is still further down, the run says how many pages it reached, charges nothing for them, and the resumeCursor below continues from exactly there.

Two honest numbers ride every finished archive:

  • The depth meter. Instagram's public feed exposes slightly fewer posts than the profile's own counter for many accounts (a profile counting 718 serves 693 — no login changes that). When the feed genuinely ends, the profile row says exactly that: "Instagram's feed exposes 693 of the 718 posts the profile counts." Complete archive, gap named, nothing silently missing.
  • The resume point. If an archive outruns your time or cost limit, the final summary row carries a resumeCursor. Pass it back as input — {"resumeCursor": …} — and the next run continues exactly where the last one stopped: completed profiles skipped, no post read twice, nothing already delivered charged again. For agents: re-run with the summary's resumeCursor until it comes back null — the archive is then complete.

The limits are hard, and they name themselves

Three things can stop a run: your result limit, your maximum cost per run, or your time limit. Whichever binds, the run still finishes successfully, the last row tells you which one it was and what is still pending — including the exact resumeCursor to continue — and nothing undelivered was charged. You will never have to guess which knob to raise.

The result limit is exact under concurrency too: two profiles are read in parallel, and a whole-run cap of 1 still delivers exactly one row, charged once for that post and once for the profile it came from.

And a limit set too high is never a refused run. This actor delivers up to 5,000 posts per profile and up to 100,000 posts per run, and its clock runs between 30 seconds and 1 hour. Ask for more than that — or for a shorter clock — and the run still starts: it continues at the nearest limit and leaves one uncharged row naming what you asked for and what bound it.

What it costs

One result = one delivered post or reel, with everything Instagram publishes about it in the same row. There is no second charge for "details" — no per-field, per-comment or per-image top-up. Nothing is charged for starting a run, no subscription, no third-party API key to buy.

from $0.60/1,000 posts — $0.0024 each on the free plan, falling to $0.0006 on the top Apify plan tier. Platform usage (compute + transfer) is included in the event price, so the per-post price you see is the whole price.

Each profile that delivers posts adds one profile lookup of $0.003 (flat, the same on every plan). Per-post prices are unchanged. A profile that delivers nothing — private, removed, unreachable, or nothing new on a watched list — pays no lookup. It covers the extra connections a walled profile needs to be read at all.

What can fail, and it carries no result fee when it does. These rows ship with charged: false, always:

statuswhen
not_foundno public profile at that handle — wrong, renamed or deleted
private_accountthe account is private, so its posts are not publicly readable
no_postsa real public profile with nothing on it
rate_wall / rate_limitedInstagram asked us to wait, and was still asking after retries
unavailable_schemaInstagram's own profile service is erroring for that handle
empty_response / source_unavailableInstagram answered, but with nothing readable
feed_unavailablethe "Tagged" tab could not be read — a licensed data feed is its only source, and it did not answer readably
input_errorthat input could not be used — a handle that is not a profile, or a Dataset ID that does not exist — the row names the fix
sample_settings_keptyou set filters and no profiles, so the sample ran under your settings — the row names which settings were yours
undeclared_fieldyour links arrived under another actor's field name, and were read here anyway — the row names the field this form carries

None of these rows carries the profile lookup either: it is charged only on a profile that delivered at least one post. Posts your filters excluded are also never delivered and never charged; the profile row counts them. Cap any run with Maximum cost per run (any amount from $0.05 up) and it stops cleanly before your ceiling.

A profile Instagram will not serve is retried through a licensed data feed, at the same price. When Instagram refuses a profile on every connection, or stops serving its post feed part-way through, the run collects the same posts from a licensed data feed instead — same columns, same result limit, same per-post price, no extra fee. If that cannot answer either you get exactly what you got before: an uncharged row saying what happened, and the resume point on the summary row.

A small ask is never asked to overpay for a hard profile. When Instagram walls a profile, the run tries further connections only while what you asked for can cover them. If it cannot — a very small result limit on a plan's lowest per-post price — the profile row says so and names the fix: ask for more posts per profile. Nothing is charged for a profile that stops there.

Honesty contract

A profile that returns nothing still gets a row, and the row says why — uncharged, every time:

statuswhat it meanscan a re-run help?
not_foundno public profile at that handle — wrong, renamed or deletedno
private_accountthe account is private, so its posts are not publicly readableno
no_postsa real public profile with nothing on it — a definitive answerno
unavailable_schemaInstagram's own profile service is erroring for that handleno, it is on their side
rate_wall / rate_limitedInstagram asked us to wait, and was still asking after retriesyes
empty_responseInstagram answered but sent an empty bodyyes
source_unavailableInstagram did not return readable data on this runyes
feed_unavailablethe "Tagged" tab's only source did not return readable data on this runyes
input_errorthat input could not be used — a handle that is not an Instagram profile, or a Dataset ID that does not exist — the row names the fixfix and re-run
undeclared_fieldyour links arrived under another actor's field name; they were read anyway and the row names the field this form carriesnothing to fix

A post that appears on two profiles you asked for (a co-authored post) is one row and one charge, not two. In the rare case Instagram stops serving a profile past every recovery, the profile row says so, is marked worth re-running, and the summary carries the exact resume point — a partial answer never reads like a complete one, and never makes you start over. When Instagram stops serving a profile's post feed part-way, you get the pages it did serve plus that resume point, and the run stops waiting on that feed after three tries that recovered nothing — so your run is not buying compute against a wall that is not lifting.

This actor may fail when the platform changes things — failed items are never charged.

Freshness

Post data is read live on every run, so it is as current as Instagram's own public profile page. The videoUrl and image links are signed by Instagram and expire in roughly 1–3 days — the videoUrlExpiresAt column tells you exactly when, so download or transcribe soon after the run rather than months later. The permalink in url never expires.

Works with

Chain this actor's output straight into the transcript actors, or feed it from any actor that emits Instagram handles.

ChainHow
Profiles → reel transcriptsrun this actor, then pass its Dataset ID to Instagram Reels Transcript Scraper — Reels to Text, No Login — it reads the videoUrl column
Any list of handles → postspass a Dataset ID here; rows need a username / ownerUsername field or an instagram.com profile URL
Paste rows directlyuse Dataset items instead of a dataset ID

A Dataset ID that does not exist is answered as a wrong ID, on one uncharged row naming it — never as "your rows carried no profiles", and no empty dataset is created under the typo. A chained dataset holding more than 5,000 rows is read to that limit and one uncharged row says how many rows it holds and how many were read, so a chained run never comes back quietly short.

Steadyfetch Instagram suite

Handle in, structured rows out. All-inclusive pay per event, nothing charged for starting a run, charged only on delivery. Profile posts adds one profile lookup of $0.003 per profile that delivers posts; a profile that delivers nothing pays nothing.

What you wantActor
Reels turned into text, with the 3-second hookInstagram Reels Transcript Scraper — Reels to Text, No Login
A profile's recent posts, capped where you saythis actor

The rest of the steadyfetch shelf — same contract everywhere: all-inclusive pay per event, nothing charged for starting a run, charged only on delivery (three actors add a small delivery-conditional fee: keyword volume's fresh lookup, profile posts' profile lookup, Google Jobs' search fee — each README states it).

FamilyActors
Ad creative intelligenceFacebook · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR
Trends & keywordsGoogle Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends
YouTube transcriptsYouTube videos · YouTube channels
InstagramReel transcripts · Profile posts
JobsIndeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs
AmazonProducts · Search · Bestsellers · Sellers
Any media fileSpeech to Text · any link or file

Feedback & support

Found an issue? Open it on the Issues tab — we usually reply within a couple of hours, always within a day. Include the input you used. Bugs on our side are fixed; misses on Instagram's side are documented in the row you already have.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.