Instagram Hashtag Scraper — Posts by Hashtag or Keyword avatar

Instagram Hashtag Scraper — Posts by Hashtag or Keyword

Pricing

from $0.60 / 1,000 hashtag posts

Go to Apify Store
Instagram Hashtag Scraper — Posts by Hashtag or Keyword

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

Steadyfetch Team

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

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 maxTotalChargeUsd on 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":

ColumnWhat it holds
shortCode · urlthe post's own code and its permalink
caption · hashtags · mentionsthe caption, and the tags and @handles pulled out of it
takenAtwhen it was posted, ISO-8601 UTC
type · productType · isVideoimage, video or carousel, and whether it is a reel (clips)
likeCount · commentCount · playCount · videoDurationengagement, and the length of a video
ownerUsername · ownerFullName · ownerId · ownerIsVerifiedwho posted it
displayUrl · videoUrl · videoUrlExpiresAtthe media links, and when the signed link stops working
taggedUsers · coauthors · locationName · carouselCounttagged accounts, co-authors, place, carousel size
hashtag · keyword · locationwhich of your terms found it — location is the location ID it was collected under
source · postSourcesource is always instagram; postSource is hashtag, keyword-search or location
charged · status · statusReasonwhether 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 typeWhat happens
103912118089363the Instagram location ID is used as it stands, with no lookup
an /explore/locations/103912118089363/eiffel-tower/ link, copied from Instagram's own place pagethe ID is lifted out of the link
Eiffel Towerone 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 planPer postPer 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.

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.