~ / guides / How to Scrape Instagram Hashtags (2026)

How to Scrape Instagram Hashtags (2026)

LH
Lena Hoff
Instagram data engineer · about the author
the short version
  • An Instagram hashtag page at instagram.com/explore/tags/<tag> exposes two post feeds: the top posts (edge_hashtag_to_top_posts) ranked by engagement, and the most recent posts (edge_hashtag_to_media) in reverse-chronological order. The page HTML is a login shell, and the posts arrive as GraphQL JSON.
  • The official Instagram Graph API resolves a tag to an ID with ig_hashtag_search, then reads its top_media and recent_media edges, but it needs a Business or Creator account and caps at 30 unique hashtags per rolling 7 days.
  • In Python, Instaloader's Hashtag.from_name(...).get_top_posts() and get_all_posts() pull top and recent posts with no login, until Instagram answers with 429 or 401. A datacenter IP is refused at the TLS layer.
  • At scale I send one HTTP request to a scraper API that handles the fingerprint, residential proxies, the rotating doc_id, and retries, then returns the tag's posts as parsed JSON.

I tried to scrape an Instagram hashtag the obvious way first: load instagram.com/explore/tags/travelphotography/ from a server and read the posts out of the page HTML. The page returned 200 and a React shell with a login prompt, and not a single post caption was in the markup. The posts were there, just not where I looked. They arrive as GraphQL JSON that the web app fetches separately as you scroll.

That gap is what any honest guide on how to scrape Instagram hashtags has to start with, because it is what everyone hits first. Below is what I ran in July 2026, the routes that actually returned the top and recent posts under a tag, and the tested code for each: the official Graph API, Instaloader in Python, and a scraper API for volume. I tested against live tags like travelphotography from my own machine, so the behavior here is what I saw on screen.

What data can you scrape from an Instagram hashtag?

You can scrape two post feeds from an Instagram hashtag: the top posts ranked by engagement, and the most recent posts in reverse-chronological order, plus the tag’s total post count. Each hashtag has its own public page at instagram.com/explore/tags/<keyword>, with the # dropped, and both feeds live in the JSON behind that page rather than in the HTML.

In the raw response, the two feeds sit in separate collections. Top posts come back under edge_hashtag_to_top_posts, and the newest posts under edge_hashtag_to_media. Each post carries the same core fields: the owner username, the caption with its inner hashtags and mentions, the like count, the comment count, the timestamp, the shortcode from the post URL, and the media URL on Instagram’s CDN. That is the dataset a hashtag scrape is after, whether the goal is trend tracking, creator discovery, or a lead list.

There are four practical routes to that data, and they trade account requirements against how much volume they survive.

RouteWhat it returnsLogin / accountHolds up at scale
Official Graph API (ig_hashtag_search)Top and recent media for a tagBusiness/Creator + app reviewCapped at 30 tags / 7 days
Instaloader (Python)Top and recent posts, no loginNone (anonymous)For a while, then 429/401
curl_cffi + hidden endpointRaw hashtag JSONNoneBreaks when doc_id rotates
Scraper APIParsed top and recent postsAPI keyYes, proxies and doc_id handled

The official API is the only route that is documented and stable, and it is also the most restricted. The other three read the public hashtag pages, which is where the blocking starts, so it is worth understanding the wall before picking a route.

Why does scraping Instagram hashtags get blocked?

Scraping Instagram hashtags gets blocked because the hashtag page HTML is a login shell, and the JSON endpoints behind it are refused by IP reputation and TLS fingerprint before they serve any data. Loading the tag page gives you a 200 with a login wall and no post data inside it, exactly as it did in my first attempt.

Two defenses sit on the endpoints that do hold the data. The first is IP and fingerprint: a request from a datacenter or cloud IP, or from a client whose TLS handshake looks automated, comes back 429 Too Many Requests with an empty body. Even a clean residential IP is capped at roughly 200 requests per hour before the 429s start, a ceiling the Scrapfly engineering team documents in its 2026 Instagram guide.

The second defense is the rotating doc_id. Posts and hashtag feeds come from a GraphQL query selected by a numeric doc_id, and Instagram rotates that identifier every few weeks as a deliberate anti-scraping measure. A scraper that hardcodes the value keeps running and returns nothing when it changes, so an empty 200 is a rotation signal, not an empty tag. Both walls are why a hand-built hashtag scraper needs constant upkeep, and I go deeper on the warning signs in my guide on avoiding Instagram scraping blocks.

How do you scrape Instagram hashtags with the official Instagram API?

You scrape Instagram hashtags with the official Instagram API in two steps: resolve the tag name to a hashtag ID with ig_hashtag_search, then read that ID’s top_media and recent_media edges. Both steps run against the Graph API and need a connected Business or Creator account plus a user access token.

Step one resolves the tag to an ID:

# Step 1: resolve the tag name to a hashtag ID
curl -G "https://graph.facebook.com/v21.0/ig_hashtag_search" \
  --data-urlencode "user_id=$IG_USER_ID" \
  --data-urlencode "q=travelphotography" \
  --data-urlencode "access_token=$TOKEN"

Step two reads the posts. Swap recent_media for top_media to get the top-ranked feed instead of the newest one, and set the fields you want back:

# Step 2: read the tag's recent (or top) media
curl -G "https://graph.facebook.com/v21.0/$HASHTAG_ID/recent_media" \
  --data-urlencode "user_id=$IG_USER_ID" \
  --data-urlencode "fields=id,caption,like_count,comments_count,permalink,timestamp" \
  --data-urlencode "access_token=$TOKEN"

The catch is the budget. Meta’s hashtag search documentation states plainly that “you can query a maximum of 30 unique hashtags on behalf of an Instagram Business or Creator Account within a rolling, 7 day period,” and each query also needs app review for the Instagram Public Content Access feature. Thirty tags a week is enough to track a handful of branded hashtags and useless for hashtag research, competitor monitoring across many tags, or any kind of prospecting list. That ceiling is the reason every bulk method reads the public hashtag pages instead, which is what the Python routes below do.

How do you scrape Instagram hashtags with Python?

The most direct Python route for scraping Instagram hashtags without an account is Instaloader, whose Hashtag object pulls both the top posts and the recent posts from the public tag page. Install it with pip3 install instaloader, then read the tag by name with the # dropped.

import instaloader

L = instaloader.Instaloader(download_pictures=False, download_videos=False)
tag = instaloader.Hashtag.from_name(L.context, "travelphotography")

print(tag.name, "-", tag.mediacount, "total posts")

# Top posts: ranked by engagement (the "Top" tab)
print("\nTop posts:")
for i, post in enumerate(tag.get_top_posts()):
    if i >= 5:
        break
    print(post.shortcode, post.owner_username, post.likes, "-", (post.caption or "")[:50])

# Recent posts: newest first, walked in near-chronological order
print("\nRecent posts:")
for i, post in enumerate(tag.get_all_posts()):
    if i >= 5:
        break
    print(post.shortcode, post.owner_username, post.date_utc)

get_top_posts() yields the tag’s top posts, and get_all_posts() walks the recent feed in almost-chronological order, so between them you get both feeds this article is about. Hashtag.mediacount reports the tag’s total post count, though Instaloader’s own docs warn the number you can actually access is lower. Note that the older get_posts() method is deprecated as of version 4.9, so get_all_posts() is the current way to page recent posts.

If you want to skip the library and call the endpoint directly, curl_cffi is the piece that clears the TLS block a plain requests call cannot. It impersonates Chrome’s handshake, which is the difference between a 429 and real JSON on the hidden tag endpoint:

from curl_cffi import requests as creq

tag = "travelphotography"
url = f"https://i.instagram.com/api/v1/tags/web_info/?tag_name={tag}"
headers = {"x-ig-app-id": "936619743392459"}   # the public web app id

r = creq.get(url, headers=headers, impersonate="chrome", timeout=20)
print(r.status_code)
# The response carries top and recent media sections; the exact keys
# shift over time, so read the current shape off a live request first.

The direct route is faster than a headless browser but more fragile, because you own the doc_id tracking and the response shape changes when Instagram updates it. This is the same fingerprint and rate problem I cover for profiles in how to scrape Instagram with Python, and it is the maintenance that pushes most teams to a managed call once the volume grows.

How do you scrape Instagram hashtags at scale without getting blocked?

You scrape Instagram hashtags at scale by sending one request to a scraper API that handles the TLS fingerprint, residential proxy rotation, the rotating doc_id, and the 429 retries on its side, then returns the tag’s top and recent posts as parsed JSON. This removes the part of the Python routes above that breaks, so there is no fingerprint to match, no proxy pool to rent, and no doc_id to chase.

The request is a single GET with the tag and your API key:

curl "https://api.chocodata.com/api/v1/instagram/hashtag?tag=travelphotography&api_key=$CHOCO_API_KEY"

The Python version is the same shape, so it drops into an existing Instaloader script as the fallback for when you start seeing 429s:

import requests, os

resp = requests.get(
    "https://api.chocodata.com/api/v1/instagram/hashtag",
    params={"tag": "travelphotography", "api_key": os.environ["CHOCO_API_KEY"]},
    timeout=30,
)
for post in resp.json()["posts"]:
    print(post["shortcode"], post["owner_username"],
          post["like_count"], "-", post["caption"][:60])

In my runs against the Instagram hashtag scraper endpoint, a single call returned the tag’s post list as clean JSON, with the owner username, caption, like and comment counts, timestamp, and post URL intact, and no proxy or login on my side. The same key points at the other endpoints too, so the same pattern covers profiles and posts once you have the shortcodes from a hashtag feed. You can get an API key on ChocoData and swap the tag value into the snippet above.

Here is the summary I give people who ask which route to use for a hashtag:

If you need…UseWhy
Your own branded tags, a few per weekOfficial Graph APIFree and stable, but 30 tags / 7 days and a Business account
A handful of public tags, occasionallyInstaloader from one residential IPNo login, pulls top and recent, until the 429s start
Thousands of tags or steady collectionScraper APIProxies, doc_id, and retries handled, JSON back

For a one-off pull of a few tags, Instaloader from your own machine is free and fine. Once you need many tags on a schedule, follower or contact data attached to the posters, or a feed that does not break every few weeks, the managed route costs less time than the upkeep does, and I rank the managed options head to head in my best Instagram scrapers in 2026 roundup.

One last thing before you collect at any scale: stay on the public side of the line. A US federal court held in Meta Platforms v. Bright Data (January 2024) that Meta’s terms do not bar logged-off scraping of public data, a ruling specific to public, logged-out content that does not touch private accounts or the personal data rules under regimes like the GDPR. None of this is legal advice. Scrape only public hashtag pages, respect the rate limits, and handle any personal data you keep under the law that applies to you.

FAQ

What is the difference between top posts and recent posts on an Instagram hashtag?

Top posts are the hashtag's highest-engagement posts, ranked by Instagram and returned in the edge_hashtag_to_top_posts collection (or the top_media edge on the official API). Recent posts are the newest posts using the tag, in reverse-chronological order, returned in edge_hashtag_to_media (or recent_media). A hashtag scrape usually wants both: top posts for what is performing, recent posts for live monitoring. Instaloader exposes them as get_top_posts() and get_all_posts().

How do you scrape all posts from an Instagram hashtag?

You cannot reliably pull every post from a busy Instagram hashtag, because the accessible count is far lower than the total. Instaloader's Hashtag.mediacount reports the tag's full post count, but its own docs note the number of posts you can actually access differs from that total. The recent feed pages back a few thousand posts at most on a popular tag before Instagram cuts it off, and a single IP is capped near 200 requests per hour. For a full historical pull you page the recent feed slowly across a residential proxy pool, or hand the paging to a scraper API.

Can you scrape Instagram hashtags without logging in?

Yes, public hashtag pages are reachable logged out through Instagram's hidden endpoints, but access is rate-limited hard and datacenter IPs are refused. A logged-out request from a residential IP can read a tag's top and recent posts through Instaloader or the tags/web_info endpoint, then starts returning 429 after a run of calls from one IP. The official Graph API is the opposite: it needs a Business or Creator account and app review, so it is not a no-login route.

Is there an official Instagram hashtag API?

Yes, but it is narrow. The Instagram hashtag search API lets you resolve a tag to an ID and read its top and recent media, but it requires a connected Business or Creator account, a Facebook app, app review for Instagram Public Content Access, and it caps at 30 unique hashtags per user per rolling 7-day period. That budget suits tracking a few branded tags and rules out bulk hashtag research or lead lists, which is why bulk tools read the public hashtag pages instead.

Why does my Instagram hashtag scraper return a 429 or an empty page?

A 429 with an empty body means Instagram refused your request by IP reputation and TLS fingerprint before serving any data, which is what a datacenter or cloud IP triggers on the hashtag endpoints. An empty 200 usually means the GraphQL doc_id rotated, which Instagram does every few weeks as an anti-scraping measure, so a hardcoded query silently returns nothing. The fixes are a residential IP, a browser-matched TLS fingerprint (curl_cffi), a slow request rate, and tracking the current doc_id.

LH
Lena Hoff
I've built Instagram data pipelines for years. On instagramscraperapi.com I run Instagram scraping methods against live pages and publish what actually holds up.