Instagram Post Scraper
Scrape Instagram posts and reels with no login: caption, like and comment counts, publish date, every carousel image and video URL, owner and hashtags.
How it works
- 1Open it on Apify
Hit Run on Apify — it opens the tool in the cloud, no install.
- 2Set the inputs
Adjust
postUrls,username,resultsLimit(sensible defaults are pre-filled). - 3Click Run
The tool runs on Apify’s cloud and collects the data for you.
- 4Export the results
Download as JSON, CSV or Excel, or pipe straight into your app, Google Sheets, or an AI agent.
Inputs
| Field | What it does | Type |
|---|---|---|
postUrls | Instagram post, reel or TV URLs. A bare shortcode such as DbtErSrlB2J also works. One row is returned per post. | array |
username | Same field name as the market-leading Instagram post scraper, so its input works here unchanged. Accepts usernames, profile URLs and post URLs mixed together. | array |
resultsLimit | How many posts to take from each username or profile URL. Ignored when you pass post URLs. Instagram serves about 12 recent posts per profile without a login, so higher values will not return more. | integer |
maxItems | Hard cap on charged rows for the whole run, across every input. Leave empty for no cap. | integer |
onlyPostsNewerThan | Keep only posts published on or after this moment. Accepts YYYY-MM-DD (2026-01-01), a full ISO timestamp, or a relative value such as 3 days, 2 months, 1 year. Filtered-out posts are never charged. | string |
skipPinnedPosts | Drop posts a profile has pinned to the top of its grid. Only detectable in profile mode. | boolean |
concurrency | How many posts to fetch at once. 6 is a good balance; lower it if you see address rotations in the log. | integer |
requestDelayMs | Optional pause after each request. Leave empty for no delay. | integer |
What you get
A structured dataset — each result includes fields like:
ownerUsernametypecaptionlikesCountcommentsCountchildPostsCounttimestampurlExport every run as JSON, CSV or Excel, or send it to your app, a database, Google Sheets, or an AI agent.
Related tools in Social Media Scrapers
Other ready-to-run tools in the same category — all pay-per-use on the Apify cloud.
Instagram Profile Scraper
Bulk Instagram profile scraper. Usernames or profile URLs in, one row out: bio, exact follower, following and post counts, verified flag, category, e-mail.
Instagram Profile Scraper
Scrape Instagram profiles by username: bio, followers, following, posts count, verified flag, business category, bio links and HD avatar. No login.
Instagram Reel Scraper
Scrape any public Instagram profile's reels: play counts, likes, comments, captions, duration, MP4 and cover links, audio track. No login or API key.
Instagram Reel Scraper
Scrape any public Instagram profile's reels: play counts, likes, comments, duration, video and cover URLs, audio track. No login or API key.
LinkedIn Post Search Scraper
Scrape public LinkedIn posts by keyword: author, text, exact date, reactions, comments and post URL. No login or cookies.
LinkedIn Profile Search Scraper
Find public LinkedIn profiles by keyword, job title, company, school or location. Get name, headline, location, current role and profile URL.
Where this tool sits
- Categories
- Social Media Scrapers
Instagram Post Scraper
Paste Instagram post or reel URLs — or just usernames — and get one clean row per post: the full caption, like and comment counts, the publish timestamp, every image and video URL in a carousel, the owner, hashtags, mentions and tagged users.
No Instagram account. No cookies. No session token. No captcha to solve. No proxy of your own.
Price
$0.09 per 1,000 posts, plus $0.0000125 to start a run.
This is the cheapest Instagram post scraper on the market, and the rate is flat: the same $0.09 on the free plan as on any paid plan, with no volume tiers and no minimum spend, so it is what you pay from your very first run.
| Posts | Cost |
|---|---|
| 100 | $0.009 |
| 1,000 | $0.09 |
| 10,000 | $0.90 |
| 100,000 | $9.00 |
What actually bills
post-scraped— $0.00009. Charged once per genuine post row written to the dataset. That is
the only per-result charge; there is no separate "filtered out" fee, no "restricted profile" fee and no dataset-item fee stacked on top.
apify-actor-start— $0.0000125. Charged once per run, per GB of run memory. The Actor runs
at 1 GB by default, so that is $0.0000125 a run.
Free, always:
- The sample row you get from an empty run.
- Every diagnostic row — rate limits, private accounts, deleted posts, bad input. If Instagram
gives nothing back, you pay the start fee and nothing else.
- Posts dropped by your own
onlyPostsNewerThanorskipPinnedPostsfilters. They are fetched and
thrown away at our expense, not billed to you.
There is no free tier and no free trial. A run that returns 40 posts costs 40 × $0.00009 = $0.0036, plus the start fee. That is the whole of it.
Input
Two ways in, and you can mix them in one run.
{
"username": ["nasa", "https://www.instagram.com/natgeo/"],
"resultsLimit": 12,
"onlyPostsNewerThan": "7 days"
}
{
"postUrls": [
"https://www.instagram.com/p/DcA6fTgIJ97/",
"https://www.instagram.com/reel/Db_sQbigDOA/",
"DcCUKZoAS8h"
]
}
| Field | What it does |
|---|---|
username | Usernames, profile URLs, or post URLs — mixed freely. The field name follows the common convention used across Instagram post Actors, so an input written for one of those normally runs here unchanged. |
postUrls | Post, reel or TV URLs. A bare shortcode such as DcCUKZoAS8h works too. directUrls and startUrls are accepted as aliases. |
resultsLimit | Posts to take from each profile. Default 20, but logged-out Instagram serves about 12 — see Limits. Ignored when you pass post URLs. |
maxItems | Hard cap on charged rows for the whole run, across every input. |
onlyPostsNewerThan | 2026-01-01, a full ISO timestamp, or a relative value like 3 days, 2 months, 1 year. Older posts are dropped and never charged. |
skipPinnedPosts | Drop the posts a profile has pinned to the top of its grid. Detectable in profile mode only. |
concurrency | Posts fetched at once. Default 6. |
requestDelayMs | Optional pause after each request. Default none. |
proxyConfiguration | Leave it alone unless you have a reason. Default is rotating Apify datacenter addresses, which is what works here and costs nothing per GB. |
Run it with no input at all and you get one labelled sample row, uncharged, showing every field — so you can see the shape before spending anything.
Output
One row per post. A real row, from a run on natgeo on 15 August 2026 (the signed query strings on the CDN URLs are shortened here to ?<signed> for readability — the real output carries them in full):
{
"ok": true,
"inputUrl": "https://www.instagram.com/natgeo/",
"id": "3963424910027562875",
"shortCode": "DcA6fTgIJ97",
"url": "https://www.instagram.com/p/DcA6fTgIJ97/",
"type": "Sidecar",
"productType": "carousel_container",
"isReel": false,
"caption": "Photos by @joelsartore | Out of 33 species of jerboa, the long-eared jerboa wins the award for most oddly proportioned family member. ... #PhotoArk",
"hashtags": ["PhotoArk"],
"mentions": ["joelsartore", "moscow_zoo_official"],
"likesCount": 42094,
"commentsCount": 180,
"likesAndViewCountsDisabled": false,
"isCommentsDisabled": false,
"timestamp": "2026-08-14T19:00:01.000Z",
"timestampSource": "instagram",
"displayUrl": "https://scontent-atl3-1.cdninstagram.com/v/t51.82787-15/766106700_18686094097019133_2576347057804717679_n.jpg?<signed>",
"videoUrl": null,
"videoDuration": null,
"images": [
"https://scontent-atl3-1.cdninstagram.com/v/t51.82787-15/766106700_18686094097019133_2576347057804717679_n.jpg?<signed>",
"https://scontent-atl3-1.cdninstagram.com/v/t51.82787-15/774970374_18686094109019133_2450257321358972437_n.jpg?<signed>"
],
"childPosts": [
{
"id": "3963424850577470287",
"shortCode": "DcA6ecIoDNP",
"type": "Image",
"displayUrl": "https://scontent-atl3-1.cdninstagram.com/v/...jpg?<signed>",
"videoUrl": null,
"alt": "Photo by National Geographic on August 14, 2026. May be an image of rodent and text.",
"width": 1440,
"height": 960
},
{
"id": "3963424852959856137",
"shortCode": "DcA6eeWoIYJ",
"type": "Image",
"displayUrl": "https://scontent-atl3-1.cdninstagram.com/v/...jpg?<signed>",
"videoUrl": null,
"alt": "Photo by National Geographic on August 14, 2026. May be an image of rodent and text.",
"width": 1440,
"height": 960
}
],
"childPostsCount": 2,
"dimensionsWidth": 1440,
"dimensionsHeight": 960,
"alt": "Photo by National Geographic on August 14, 2026. May be an image of text.",
"ownerId": "787132",
"ownerUsername": "natgeo",
"ownerFullName": "National Geographic",
"ownerIsVerified": true,
"ownerIsPrivate": false,
"ownerProfileUrl": "https://www.instagram.com/natgeo/",
"coauthorProducers": [],
"taggedUsers": [],
"locationName": null,
"locationId": null,
"musicArtist": null,
"musicTitle": null,
"source": "post-page",
"scrapedAt": "2026-08-15T05:43:15.443Z"
}
images is the flat list of every media URL in the post — one entry for a single image or video, one per slide for a carousel. childPosts is the same thing with the per-slide detail: its own id, shortcode, type, dimensions and alt text. A 20-slide NASA carousel came back with all 20.
type is Image, Video or Sidecar (carousel) — the conventional naming for Instagram post data, so existing pipelines do not need remapping.
Field coverage, measured
From the 15 August 2026 test run over nasa and natgeo — 12 posts, 6 through each of the two paths the Actor uses:
| Field | Present |
|---|---|
shortCode, url, type, caption, likesCount, commentsCount, timestamp | 12/12 |
ownerUsername, ownerId, ownerProfileUrl, displayUrl, images | 12/12 |
ownerFullName, ownerIsVerified | 11/12 — missing on the one coauthored post owned by a third account |
videoUrl on type: "Video" rows | 6/6 |
videoDuration | video rows fetched by post URL only |
taggedUsers, locationName, musicTitle | only when the post has them; [] or null otherwise |
Missing values are null or []. Nothing is guessed.
Which input mode should you use?
The two modes answer different questions, and the cost profile is different too.
username (profile mode) | postUrls (URL mode) | |
|---|---|---|
| Best for | "what has this account posted lately" | "give me exactly these posts" |
| Depth | about 12 most recent posts, logged out | unlimited — as many URLs as you pass |
| Requests per post | one profile request returns the whole grid | one request per post |
videoDuration | not available | returned |
skipPinnedPosts | works | not detectable |
onlyPostsNewerThan | works, and filtered posts are free | works, and filtered posts are free |
Profile mode is the cheaper and faster of the two per post, because a single request brings back the whole recent grid. URL mode is the one to use for back-catalogue work, for a fixed list of posts you already know, or when you need videoDuration. You can mix both in one run — pass username and postUrls together and the results land in the same dataset.
A worked example. Monitoring 50 accounts once a day in profile mode is roughly 50 × 12 = 600 posts, or $0.054 a day plus the start fee — about $1.65 a month for a daily brand-monitoring feed. Add onlyPostsNewerThan: "1 day" and you only pay for posts that are actually new, which for most accounts brings that down by an order of magnitude.
Limits
Read this before you build on it.
- About 12 posts per profile, logged out. Instagram's public profile response carries roughly the
12 most recent posts and nothing more. resultsLimit: 100 will not produce 100 — it produces the 12 that exist publicly, and you are charged for 12. Deep back-catalogue paging requires a logged-in session, which this Actor deliberately does not use. If you need specific older posts, pass their URLs into postUrls; there is no depth limit that way.
- No view or play counts. Logged-out Instagram does not include them in either response, so the
field is not emitted rather than filled with a guess. Any scraper showing you reel view counts is using an account.
- No comment text and no liker lists. Counts only.
- Private accounts return a diagnostic row, not data, and are not charged. Same for deleted posts
and handles that do not exist.
- Media URLs are signed and expire.
displayUrl,videoUrland theimageslist point at
Instagram's CDN with a signature that stops working after a few days. Download what you need soon after the run, or store the bytes rather than the link.
- Rate limits are real and per address. Instagram meters its public routes per IP. The Actor
hops to a fresh address on every block rather than sleeping, and one of the two profiles in the test run needed 15 hops before it answered. On a bad minute a profile can still come back as a RATE_LIMITED diagnostic row — which is free, and re-running usually clears it.
videoDurationonly on the post-URL path. The profile response does not carry the DASH
manifest it is read from.
- Engagement counts drift. Two runs a minute apart on the same NASA post returned 88,713 and
89,083 likes. That is Instagram, not the scraper.
FAQ
Do I need an Instagram account, cookies, or a session ID? No. Nothing to log in to, nothing to paste, no account of yours to put at risk.
What does it cost to scrape 1,000 Instagram posts? $0.09, plus $0.0000125 to start the run.
Is this the cheapest Instagram post scraper? Yes. $0.09 per 1,000 posts is below every other Instagram post scraper on the Apify store, and below every actor of any kind returning Instagram posts.
Can I scrape reels? Yes. Reel URLs work exactly like post URLs, and reels appear in profile results with isReel: true. Caption, likes, comments, the video file URL and the duration all come back.
Does it get every image in a carousel? Yes — all of them, in images and in childPosts, each with its own dimensions and alt text.
Can I filter by date? Yes: onlyPostsNewerThan takes 2026-01-01, a full ISO timestamp, or 7 days / 3 months. Posts filtered out are not charged.
What happens if I run it with empty input? You get one clearly labelled sample row explaining what to fill in, the run finishes as SUCCEEDED, and no result is charged.
Can I export to CSV or Excel? Yes — Apify exports the dataset as JSON, CSV, Excel or XML, and the same data is available from a REST endpoint.
Is this affiliated with Instagram or Meta? No. It reads public post data that Instagram serves to logged-out visitors. Check Instagram's terms and your own local rules before using the output commercially, especially for personal data.
How it works
Instagram's JSON endpoints are walled for logged-out clients. /api/v1/media/<id>/info/ answers with 400 KB of login HTML, i.instagram.com returns 403, and the public GraphQL doc_ids that scrapers used for years have been retired — they now return {"errors":[{"message":"execution error"}]}.
What is not walled is the ordinary post page. https://www.instagram.com/p/<shortcode>/ is server rendered for visitors who are not logged in, and it ships the complete media object inside its <script type="application/json"> blocks: taken_at, like_count, comment_count, the full caption, every carousel child with its own media URL, the owner, tagged users and location. This Actor parses that. For profiles it first tries web_profile_info, which returns the recent grid in one request, and falls back to reading the profile page and then each post page when that route is rate limited — which is what happened to natgeo in the final test run, and it still returned all 6 requested posts.
Two engineering notes, in case they save you time on your own build:
The transport is impit, not fetch. Plain Node returns 401 or a login wall on the first request from an address that works fine a second later through impit's Chrome TLS profile. It reads like an IP block and is not one — it is the TLS handshake. Overriding the user-agent re-breaks it, so the UA is left to match the fingerprint.
Datacenter addresses, never residential. A post page is about 115 KB on the wire, and residential bandwidth is metered by the gigabyte — at that response size the egress alone would cost more per row than a row is priced at. Rotating datacenter addresses answered every request in testing and are not metered per GB, which is exactly what a per-IP rate limit needs and the reason this price is possible at all.