Instagram Hashtag Scraper — Posts by Hashtag or Keyword
Pricing
from $0.60 / 1,000 hashtag posts
Instagram Hashtag Scraper — Posts by Hashtag or Keyword
Instagram posts and reels under any hashtag, or found by keyword search — captions, hashtags, mentions, engagement, media links, owner and location. No login. From $0.60 per 1,000 posts. Your result limit is exact: set 30 and you get 30. No start fee, and an undelivered post is never charged.
Pricing
from $0.60 / 1,000 hashtag posts
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Type the hashtags you want and get the posts and reels published under them: caption, hashtags, mentions, engagement, media links, owner, location and the permalink. Or search by keyword and get the reels Instagram ranks top for that phrase, or name a PLACE and get everything posted there. No login, no cookies, no proxy to configure. Your result limit is exact — set 30 and you get 30, and a post we could not deliver is never charged. Nothing is charged for starting a run.
Using an AI agent? Pin this actor in Apify's MCP server with one link: https://mcp.apify.com?tools=steadyfetch/instagram-hashtag-scraper
- Actor id:
steadyfetch/instagram-hashtag-scraper - Input:
{ "hashtags": ["travel"], "resultsLimit": 30 }— the one field you have to set.{ "keywords": ["van life"] }searches instead of tagging, and{ "locations": ["Eiffel Tower"] }collects a place. - Cap the bill: set
maxTotalChargeUsdon the run (a run option, not Actor input), e.g.0.50— the run stops when it reaches it.
Unofficial. Not affiliated with, endorsed by, or sponsored by Instagram or Meta Platforms, Inc.
What you get
One row per post, with everything the post exposes at one price — there is no second charge for "details":
| Column | What it holds |
|---|---|
shortCode · url | the post's own code and its permalink |
caption · hashtags · mentions | the caption, and the tags and @handles pulled out of it |
takenAt | when it was posted, ISO-8601 UTC |
type · productType · isVideo | image, video or carousel, and whether it is a reel (clips) |
likeCount · commentCount · playCount · videoDuration | engagement, and the length of a video |
ownerUsername · ownerFullName · ownerId · ownerIsVerified | who posted it |
displayUrl · videoUrl · videoUrlExpiresAt | the media links, and when the signed link stops working |
taggedUsers · coauthors · locationName · carouselCount | tagged accounts, co-authors, place, carousel size |
hashtag · keyword · location | which of your terms found it — location is the location ID it was collected under |
source · postSource | source is always instagram; postSource is hashtag, keyword-search or location |
charged · status · statusReason | whether this row was billed, and why, so your invoice reconciles from the dataset itself |
Media links are signed and time-limited by Instagram: videoUrlExpiresAt tells you exactly how long each one stays fetchable, so download what you need in the same session. The requesting network address is stripped out of every link before it reaches your dataset.
What a row looks like
One delivered row from a real run — the hashtag latteart, 2026-09-12. Every post or reel is one hashtag-post charge, and hashtag / keyword say which of your inputs found it. Media links are signed and time-limited by Instagram, so they are shown here as placeholders.
{"postId": "3984712977128543428","shortCode": "DdMi1c9otTE","url": "https://www.instagram.com/p/DdMi1c9otTE/","type": "image","productType": "feed","caption": "From blossom to berry, and from green cherry to roasted bean ☕️🌱 — every coffee bean has a journey.","hashtags": [],"mentions": [],"takenAt": "2026-09-12T17:41:13.000Z","takenAtTimestamp": 1789234873,"likeCount": 103,"commentCount": 2,"playCount": 0,"videoDuration": null,"displayUrl": "https://…","videoUrl": null,"videoUrlExpiresAt": null,"carouselCount": null,"dimensionsWidth": 1304,"dimensionsHeight": 1463,"ownerId": "20486637937","ownerUsername": "wealllovecoffee","ownerFullName": "We all love coffee","ownerIsVerified": false,"taggedUsers": [],"coauthors": [],"locationName": null,"locationId": null,"isPinned": null,"isSponsored": false,"postSource": "hashtag","profileHandle": null,"profileFullName": null,"profileFollowers": null,"profileIsVerified": null,"profilePostCount": null,"source": "instagram","hashtag": "latteart","keyword": null,"isVideo": false,"input": "latteart","sourceIndex": 0,"scrapedAt": "2026-09-12T20:42:25.359Z","repeat": false,"charged": true,"status": "delivered","retryable": false,"statusReason": null,"#k": "3984712977128543428","#ce": {"hashtag-post":1},"#ask": {"delivered":1}}
Collect by place
Put a place in Locations and you get the posts tagged at it, exactly as the hashtag door works — same price, same hashtag-post charge, same exact result limit. Three shapes are read:
| You type | What happens |
|---|---|
103912118089363 | the Instagram location ID is used as it stands, with no lookup |
an /explore/locations/103912118089363/eiffel-tower/ link, copied from Instagram's own place page | the ID is lifted out of the link |
Eiffel Tower | one lookup resolves the name to the place Instagram ranks first |
A name is ambiguous and the run says so rather than pretending it is not. Instagram ranks two different "Eiffel Tower"s — the Paris monument and one in Las Vegas — so a run that resolves a name adds one uncharged row naming the place it read, its location ID, and the other places ranked under the same name. If you meant one of those, put its ID in Locations and re-run: an ID is never resolved and never ambiguous.
A place that Instagram has nothing under is an uncharged tag_not_found row — proved by asking Instagram's own place record, never guessed from an empty grid. A place whose grid answers nothing while the place itself exists is an uncharged vendor_unavailable row and worth a re-run: a place publishes no post count, so there is no way to prove a grid is empty rather than unread, and this actor will not tell you a place is empty when it cannot know that.
Mixing works: hashtags, keywords and locations in one run share resultsLimit evenly, and every row says which of them found it.
Price
One charged event, hashtag-post, and it lands only on a post that was actually delivered.
| Apify plan | Per post | Per 1,000 posts |
|---|---|---|
| Free plan | $0.0024 | $2.40 |
| Bronze | $0.0014 | $1.40 |
| Silver | $0.0009 | $0.90 |
| Gold and above | $0.0006 | $0.60 per 1,000 |
Platform usage is included in that price — there is no separate compute bill on top, and no start fee. Set maxTotalChargeUsd on the run and it is a hard ceiling: the run stops cleanly under it and the last row says what is left.
Honest rows: what "not charged" actually means
Every run closes its own books. A post that was not delivered leaves an uncharged row saying which of nine things happened, and none of them is billed:
tag_not_found— Instagram has no such hashtag, or no place under the name or ID you gave. Proved by asking Instagram's own tag or place lookup, not guessed from an empty page.no_posts— Instagram offered no next page for the term. A definitive answer for this run, not a failure, and the only one of these that is a verdict about the hashtag itself.skipped.page_cap— this actor reads at most 40 pages for one term, and the walk reached that bound with the feed still serving. A bound of ours: ask for fewer posts per term, or give the term a run of its own, to reach further.skipped.all_repeat— three pages in a row held only posts you already have, so the run stopped reading rather than buy more pages of the same. Also a bound of ours: switch Include posts you already have on to get them delivered again.stopped_at_limit— one of your own limits, or the run clock, stopped it first. The row names which.vendor_unavailable— the read did not go through, or a place's grid came back empty while the place itself exists (a place publishes no post count, so an empty grid can never be proved to be the place's own answer). Temporary, says nothing about your hashtag, phrase or place, and a re-run is the fix. When posts had already been delivered for that term before the feed stopped answering, the row opens with how many — those rows are in your dataset and say what they were charged, and only the rest carries no result fee.source_refused— Instagram refused to look the term up at all, and it refuses it the same way every time, so a re-run cannot change it. Check the spelling in a browser; if the term works there, say so on the Issues tab.unsupported_shape— Instagram answered in a shape this actor does not read yet. That is on us, not on your term, and a re-run gets the same answer — tell us on the Issues tab which term it was and support for it will be added.vendor_budget— this actor reached its own monthly collection allowance and stopped rather than collecting more.
The last row of every run is a receipt: delivered, asked for, what was skipped, what stopped it, and the charged-event totals. A time limit ends the collecting, never the delivering — posts already in hand are always written out.
How often this data changes
A hashtag feed moves continuously: popular tags take thousands of new posts an hour, quiet ones a handful a day. A place moves more slowly and far more unevenly — a landmark or a stadium takes hundreds of posts on a busy day and almost none on a quiet one, a small venue a handful a week — so a daily schedule on a location is usually worth it only for somewhere busy, and a weekly one is the right cadence for most places. Re-running the same hashtag therefore buys mostly new posts, which is the point — but it would also re-buy the ones you already have, so it does not. Every run remembers what it delivered to your account, in the key-value store ig-hashtag-account in your own Apify account, and a later run skips those: not delivered again, not charged, and the slot goes to the next new post instead, so you still get the number you asked for. 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; without it the run says so on its status line and on an uncharged row, and cannot skip the posts you already have. That makes a daily schedule cost only what is new, as long as the token the schedule runs on can open that store. Switch Include posts you already have on to get them back anyway, marked repeat: true and still uncharged, and use Skip posts in this dataset to carry the same memory across accounts.
Reliability
The posts come from a licensed data feed rather than from scraping Instagram's own web pages, which is why there is nothing to log into and nothing to configure. When that feed cannot answer, the run says so on an uncharged row and finishes successfully — a failed read is never billed and never dressed up as an empty hashtag.
Related actors
- One account's own posts, rather than a hashtag: Instagram Profile Posts Scraper — https://apify.com/steadyfetch/instagram-profile-posts
- The spoken words inside a reel: Instagram Reel Transcript Scraper — https://apify.com/steadyfetch/instagram-reel-transcript-scraper
Limits and good manners
Your settings are never refused for being too big. This actor delivers up to 5,000 posts per run (resultsLimit) and runs for up to 1 hour (maxRunSeconds, minimum 30 seconds). 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 saying what you asked for and what bound it. Nothing is refused before it starts.
Only public posts are returned. Instagram's hashtag and place surfaces serve a ranked slice rather than a complete archive, so a very deep ask on a small tag will run out — the run says so instead of padding. This actor may stop working if Instagram changes how the data is served; if it does, failed items are never charged.
Support
Something off, or a column you need that is not here? Open an issue on the Issues tab — we usually reply within a couple of hours.
If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.