Instagram Location Scraper By Keyword & Hashtag Filter
Pricing
$19.99/month + usage
Instagram Location Scraper By Keyword & Hashtag Filter
Scrape Instagram posts by location using this Apify actor. It extracts post captions, image URLs, hashtags, usernames, geotags, and engagement data from any location page. Ideal for local marketing, trend tracking, or research with exportable results in JSON, CSV, or Excel formats.
Pricing
$19.99/month + usage
Rating
5.0
(1)
Developer
Scraper Engine
Maintained by CommunityActor stats
0
Bookmarked
41
Total users
2
Monthly active users
5 days ago
Last modified
Categories
Share
Instagram Location Scraper — Posts, Hashtags and Coordinates
Instagram Location Scraper By Keyword & Hashtag Filter collects public posts from any Instagram location feed and returns them as structured JSON rows: caption, like and comment counts, poster username, place name with latitude and longitude, media URLs, parsed hashtags and @mentions, and the people tagged in the photo. Every row also carries a passedFilter verdict and a matchedKeywords list from your own keyword and media-type filter. No HTML parsing on your side.
⚠️ Read the session-cookie requirement below before your first run. Instagram serves location feeds only to a logged-in session, so this Actor needs your own sessionid cookie. Without one it returns no posts.
⚠️ You must supply your own Instagram sessionid
sessionId is not marked required in the input schema, but it is required in practice, and that is worth stating plainly rather than leaving you to discover it on the first run.
The Actor reads Instagram's own GraphQL endpoint (https://www.instagram.com/graphql/query) for the location tab. Instagram answers an unauthenticated request with an empty edge list, and the code turns that into an explicit error: "No session ID provided. Instagram requires authentication to return posts." The keyword-search path behaves the same way — the search endpoint bounces a logged-out request to the login page. So a run without a cookie logs a warning at startup, fails each location after its retries, and finishes with an empty dataset.
Where the value comes from:
- Log in to Instagram in your browser
- Open DevTools (F12) → Application → Cookies →
https://www.instagram.com - Copy the value of the
sessionidcookie into thesessionIdinput
The field is declared isSecret in the input schema, so Apify stores it encrypted and masks it in the run's input view. Two things follow from that, and neither is a reason not to use the Actor — just something to plan for. First, a sessionid is your account credential, not an API key: anyone holding it is logged in as you, so treat it like a password and rotate it if it leaks. Second, replaying a live session from an unfamiliar IP is exactly the pattern Instagram's own risk systems watch for. Route the run through a residential proxy, keep run volumes sane, and use an account you are willing to see challenged — the Actor cannot insulate your account from Instagram's judgement, and it does not claim to.
If the cookie is expired or flagged, Instagram redirects to the login page. The Actor detects the redirect and raises "Redirected to login page. Session ID may be invalid or expired." Refresh the cookie and re-run.
What is Instagram Location Scraper By Keyword & Hashtag Filter?
Instagram Location Scraper By Keyword & Hashtag Filter is an Apify Actor that walks the post feed of an Instagram place page and shapes every post into a flat, typed dataset row with 24 keys. On top of the raw post it adds client-side annotations computed from data already fetched — parsed hashtags, parsed mentions, a taggedUsers list, and a passedFilter / matchedKeywords verdict against your keyword and media-type filter — at zero extra requests.
It does require an Instagram sessionid cookie from an account you can log into, because the location feed is login-walled. Everything it returns is public post content that any logged-in visitor sees on the place page.
It is built for local marketers profiling a venue's visitors, agencies running geo-targeted campaigns, event and tourism teams monitoring a landmark, and developers piping place-tagged content into dashboards or AI pipelines.
What Instagram location post data is publicly available to scrape?
Posts tagged at a place were published publicly by their authors, and Instagram attaches the place name and its coordinates to each one — but Instagram gates the location grid itself behind a logged-in session, and it does not put every field about a post on that grid.
| Data Category | Returned by this Actor | Gated or not extracted |
|---|---|---|
| Post media, caption and timestamp | ✅ Public | — |
| Place name, city, latitude and longitude | ✅ Public | lat / lng are null when Instagram omits them |
| Poster username, full name, id, verified flag | ✅ Public | — |
| Like and comment counts | ✅ Public | Author can hide them; isLikeAndViewCountsDisabled flags it |
| Hashtags and @mentions written in the caption | ✅ Public | — |
| People tagged in the media | ✅ Best effort | Empty when Instagram trims usertags from the feed node |
| The location feed itself | ❌ | Login-walled — needs your sessionid |
| Comment text, commenter identity, poster follower count and bio | ❌ | Not on the location feed — use a comments or profile Actor |
| Posts from private accounts, stories, DMs | ❌ | Never returned |
Instagram Location Scraper By Keyword & Hashtag Filter only returns publicly visible data — what any logged-in visitor sees on the place page. Nothing behind a follow request.
How the keyword and hashtag filter works, and what it costs
The filter is the reason to pick this Actor over a plain location scraper, so here is exactly how it behaves.
It is a post-fetch filter, not a server-side query. Instagram's location endpoint takes a location id, a page size and a sort tab — it has no keyword parameter, so nothing you type in filterKeywords or mediaType reaches Instagram. The Actor fetches the location feed first, then evaluates each post locally against your terms. Matching is case-insensitive substring matching over a haystack built from four things: the caption, the parsed hashtags, the parsed mentions, and every tagged user's username and fullName. A term found in any of them counts as a match.
Two consequences follow, and both cost you money if you do not plan for them:
- Filtered-out posts are still written to the dataset and still charged. The filter annotates; it does not suppress. Every post the Actor fetches is pushed as one
row_resultevent, whetherpassedFilteristrueorfalse. Ten matching posts out of two hundred fetched is two hundred charged rows, not ten. Drop the non-matches yourself after the run:[r for r in items if r["passedFilter"]]. maxItemscounts posts fetched, not posts kept. It is applied while paginating, before the keyword and media-type verdict exists.maxItems: 20with a narrow keyword list can easily return 20 rows of which 2 pass.
The date filter is the exception and works the other way round: dateFilterType, absoluteStartDate, absoluteEndDate, relativeValue and relativeUnit are applied during extraction, before anything is pushed, so posts outside your window never enter the dataset and are never charged. If cost control matters more than keyword precision, narrow the date window first and the keyword list second.
What data can I extract with Instagram Location Scraper By Keyword & Hashtag Filter?
Every row is one post, with 24 keys always present — post identity, media, engagement, the poster, the place, and the filter verdict.
| Field Name | Description |
|---|---|
type | Always the string post |
id | Instagram media id, as a string |
code | Instagram short code for the post |
url | Canonical post URL, built as https://www.instagram.com/p/{code}/ |
createdAt | Publish time as ISO-8601 with a trailing Z; falls back to the Unix epoch when Instagram omits the timestamp |
passedFilter | true when the post satisfies both your keyword test and your media-type test |
matchedKeywords | Which of your filterKeywords were found, in the order you supplied them; [] when you supplied none |
hashtags | Hashtags parsed from the caption, # stripped, deduplicated, order preserved |
mentions | @-mentions parsed from the caption, @ stripped, deduplicated, order preserved |
taggedUsers | People tagged in the media — array of {username, fullName, isVerified, position} |
likeCount | Like count, or null when includeEngagement is off |
commentCount | Comment count, or null when includeEngagement is off |
caption | Full caption text, "" when the post has none |
isAvailable | Always true on a returned row |
isLikeAndViewCountsDisabled | true when the author hid their counts |
isPinned | true when the post is pinned for users |
isPaidPartnership | Instagram's paid-partnership flag |
isCarousel | true for multi-item posts |
isVideo | true for videos and reels |
owner | Poster object — {id, username, fullName, profilePicUrl, isPrivate, isVerified}, or null |
location | Place object — {id, name, city, lat, lng}, or null |
video | Video object — {id, url, width, height, duration}, or null |
image | Image object — {url, width, height}, or null |
audio | Audio object — {id, title, artist, coverArt, duration, audioUrl}, or null |
Nested objects are
nullrather than absent when a post has no video, no image, no audio, no place or no resolvable owner. No key is ever omitted, so a fixed 24-column CSV export always lines up.
Post identity, media and timestamps
id, code and url locate the post; type is a constant, so branch on isVideo and isCarousel instead. image and video each hold the highest-resolution variant Instagram offered, chosen by width × height, not a list of every rendition. On a carousel, only the first slide's media is returned — remaining slides are not emitted, so a three-image carousel is one row with one image.
video.id and audio.id are synthetic labels the Actor builds from the post and asset ids, not Instagram identifiers you can look up. video.duration comes from Instagram's own duration field when present, otherwise it is parsed out of the DASH manifest's mediaPresentationDuration. audio is assembled from whichever upstream block exists — a licensed track's music-asset info, a reel's clips metadata, or the audio track's URL lifted from the DASH manifest — so audio.duration is Instagram's duration on the licensed-music path and its duration_in_ms on the original-sound path. Check the magnitude before you treat it as seconds.
Filter verdict, entities and tagged people
passedFilter and matchedKeywords are the filter's output, described in full in the section above. hashtags and mentions are regex-parsed from the caption text itself — a mention appears here only if the author typed it in the caption, which is not the same as Instagram's tagged-user list.
That list is taggedUsers, read from the feed node's usertags block, including tags carried by individual carousel slides. Each entry has username, fullName, isVerified and position — the [x, y] coordinate pair of the tag on the image, or null when Instagram does not give one. It is best-effort: Instagram frequently trims usertags out of feed responses, and the Actor makes no extra request to recover them, so [] is a normal and common value.
Engagement, poster and place
likeCount and commentCount are the metrics, both suppressed to null when you turn includeEngagement off. isLikeAndViewCountsDisabled tells you when the author, rather than your input, is the reason a count looks wrong.
owner identifies the poster; location carries the place, including lat and lng as raw decimal degrees when Instagram publishes them. Those coordinates are what make this Actor useful for venue and catchment work — and what make its output more sensitive than a plain post export. See the legal section below.
🤖 Add-on: Need additional Instagram data?
A location feed tells you who showed up, not who they are. Instagram Profile Scraper turns an owner.username into follower counts and bio data so you can qualify the accounts you discover here. Instagram Comment Leads Scraper takes a code and pulls the conversation under the post. If you want the same keyword-filter treatment applied to a tag feed instead of a place feed, Instagram Hashtag Scraper covers that side.
Why not build this yourself?
There is no shortcut through Instagram's official API for this job. The Instagram Graph API exposes media for accounts you own or manage, plus a hashtag-search surface for Business and Creator accounts — it publishes no endpoint that returns a third party's location feed. Whatever you build, you are building against the same private web endpoint this Actor uses.
That means owning four moving parts. The GraphQL query is addressed by a document id that Instagram bumps whenever it ships front-end code, so you cannot hardcode it — this Actor re-derives it every run by downloading the page's JavaScript bundles from Instagram's CDN and matching the location-tab query operation inside them. You need a live CSRF token, scraped from the page HTML on each run. You need companion cookies: a bare sessionid is not enough, because the location page bounces into a redirect loop unless csrftoken, mid, ig_did and datr are seeded first — this Actor makes one homepage request to prime them. And you need egress Instagram tolerates, which in practice means residential IPs and a proxy bill.
None of that is exotic, but all of it breaks on Instagram's schedule rather than yours. Running it here means one input form, an Apify dataset, and a stable output schema when Instagram moves the parts around underneath.
How to use Instagram Location Scraper By Keyword & Hashtag Filter
The Actor runs on Apify. Start it from the Apify Console or call it through the Apify API — there is no separate signup and no credential besides your Apify token and your own Instagram cookie.
- Open the Actor on Apify and click Try for free
- Paste your Instagram
sessionidcookie into Instagram sessionid Cookie - Add one or more entries to Location URLs, Usernames or Keywords —
startUrlsis the one schema-required input. A run started without it logs "No startUrls provided" and exits immediately with an empty dataset and no charge - Optionally add terms to Keyword Filter and pick a Media Type to set the
passedFilterverdict - Set Post Limit, and set a date window if you want one — remember
relativeValuedefaults to0, which means no date filtering - Turn Proxy Configuration on and select RESIDENTIAL; Instagram login-walls datacenter IPs
- Click Start, then download the dataset as JSON, CSV or Excel, or read it through the Apify API
How to scale to bulk location extraction
startUrls is a list, so bulk is the normal mode — add as many entries as you like and they are processed in order. Be aware that maxItems is a run-wide budget, not a per-location one: with maxItems: 20 and five locations, the first location can consume the entire allowance and the remaining four are skipped with "Reached max items limit. Skipping remaining locations." Give a multi-location run a proportionally larger limit, or use 0 for unlimited.
One keyword entry can also fan out on its own: a place-name search returns up to 10 matching locations, an Actor-set cap you cannot change from the input, and each of those locations then draws on the same shared maxItems budget.
What can you do with Instagram location post data?
- 📍 A venue marketing manager profiling their own location runs the place URL with
filterKeywordsset to their brand terms, then keeps rows wherepassedFilteristrueand readsowner.usernameandcaptionto find customers already posting about them by name. - 🗺️ A tourism board analyst measuring landmark seasonality runs the same location with
dateFilterTypeset toabsoluteacross two summers and compareslikeCount,commentCountand post volume perlocation.namebetween the windows. - 🎥 A short-form content strategist sets
mediaTypetovideoso only reels earnpassedFilter: true, then groups the passing rows byaudio.titleto see which tracks are carrying reach at that place this month. - 🏷️ An influencer marketer building a local roster collects
taggedUsersandmentionsacross several neighbourhood locations, counts which handles recur, and hands the shortlist to a profile Actor for follower numbers. - 🤖 An AI engineer building a geo-aware monitoring agent indexes
caption,hashtagsandlocation.nameinto a vector store withlocation.latandlocation.lngas metadata, so the agent can answer "what are people posting near this venue this week" against live content rather than a stale export.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.
How does Instagram Location Scraper By Keyword & Hashtag Filter handle rate limits and blocking?
Every request carries a full desktop Chrome header set — user agent, client hints, fetch metadata and a live CSRF token — and travels on a session that has already visited the Instagram homepage, so the companion cookies Instagram expects are present before the location page is touched.
Egress runs a fallback ladder: no proxy → Apify datacenter → Apify Residential. If you select RESIDENTIAL in the input, the Actor starts there instead of climbing, and once a residential IP returns posts it becomes sticky for the rest of the run. A response is classified as blocked on HTTP 403, 429 or 503, or on an error message containing blocked, forbidden, rate limit, too many requests, redirect or login. A blocked request moves to the next rung, and residential is retried at most three more times before that location is abandoned.
Each network stage — the page fetch, the CSRF-token fetch and the GraphQL page fetch — gets up to 3 attempts with a 2s / 4s / 6s backoff. Redirects are followed manually with a hard cap of 6 hops, because Instagram's location page loops against the HTTP client's automatic follower. Page and token requests use a 30-second total timeout with a 10-second connect timeout.
There is no CAPTCHA solving in this Actor, and none is claimed. When a location cannot be fetched after its retries, that location is logged and skipped, the run continues with the remaining locations, and rows already collected are kept. Note that an expired cookie is classified as "blocked" too, because it manifests as a login redirect — so it will burn the whole proxy ladder before failing, and no IP change can fix it. Refresh the cookie instead.
⬇️ Input
One parameter is required: startUrls. Everything else has a default.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
startUrls | Yes | array | Instagram location URLs, @usernames, or place keywords — the input type is auto-detected per entry. Prefilled with one Berlin location URL. | ["https://www.instagram.com/explore/locations/213131048/berlin-germany/"] |
maxItems | No | integer | Maximum posts to scrape per run before filtering. 0 = unlimited. Minimum 0, default 20. | 50 |
sessionId | No | string | Your Instagram sessionid cookie value. Required in practice — logged-out location feeds are login-walled and return no posts. Stored encrypted. | "51234567890%3AqR7tVbNm2XyZaP%3A17" |
filterKeywords | No | array | Keep-flag terms. A post whose caption, hashtags, mentions or tagged-user names contain any term is marked passedFilter: true and lists its hits in matchedKeywords. Case-insensitive. Default [], which passes every post. | ["brunch", "coffee"] |
mediaType | No | string | Which media type counts as a match for passedFilter. Enum: all, image, video, carousel. Default "all". | "video" |
extractEntities | No | boolean | Parse #hashtags and @mentions out of each caption. Turn off to leave both arrays empty. Default true. | true |
includeTaggedUsers | No | boolean | Collect people tagged in the media into taggedUsers. Turn off to leave the array empty. Default true. | true |
includeEngagement | No | boolean | Include likeCount and commentCount. Turn off to set both to null. Default true. | true |
includeVideoMetadata | No | boolean | Include video.width, video.height and video.duration. Turn off to keep only video.id and video.url and null the rest. Default true. | true |
proxyConfiguration | No | object | Apify Proxy settings. Prefilled with {"useApifyProxy": false}. RESIDENTIAL is what Instagram effectively needs. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
dateFilterType | No | string | Date filter mode. Enum: absolute, relative. Default "relative". | "relative" |
absoluteStartDate | No | string | Keep posts on or after this date, YYYY-MM-DD. Used when dateFilterType is absolute. Default "". | "2026-05-01" |
absoluteEndDate | No | string | Keep posts on or before this date, YYYY-MM-DD. Used when dateFilterType is absolute. Empty means now. Default "". | "2026-07-01" |
relativeValue | No | integer | How many units back to keep posts from. Used when dateFilterType is relative. Minimum 0, default 0 — and 0 means no date filtering at all. | 30 |
relativeUnit | No | string | Unit for the relative window. Enum: days, weeks, months, years. Default "days". | "days" |
Seven honest notes on how these behave:
- What
startUrlsactually accepts. An entry is treated as a location URL only if it starts withhttp://orhttps://and contains/locations/; it is then used verbatim, and the numeric id is pulled out with the pattern/locations/(\d+)/— the trailing slash after the id is part of that pattern, so a URL ending.../locations/213131048without it fails and the entry is skipped. An entry is treated as a username only if it starts with@. Everything else is treated as a place keyword, including a baremrbeastand a bare numeric id like213131048: neither is recognised as a username or a location id, and both are sent to Instagram's place search as text. Prefix usernames with@, and pass location ids as full URLs. - Any other Instagram URL is silently searched as a keyword. A profile or post URL does not contain
/locations/, so it falls through to the keyword branch and is searched as a place name — usually returning nothing, with only a log line to explain it. - There is one hardcoded location fallback. If a keyword search returns no locations and the keyword is exactly
berlin, the Actor substitutes location id213131048(berlin-germany) and scrapes that instead. It is the only entry in the fallback table; every other keyword that finds nothing simply yields no locations. - The
@usernamepath is uncapped. It reads the profile HTML and turns every location link it finds into a location to scrape, with no Actor-side ceiling on how many. Only the run-widemaxItemsbudget stops it. maxItemshas two different defaults. The schema default is20, which is what the Console sends. If you build the input JSON yourself and omit the key entirely, the code falls back to100.- The four toggles do not save any work or cost.
extractEntities,includeTaggedUsers,includeEngagementandincludeVideoMetadataare applied after extraction — the values are parsed either way and then blanked. Turning them off narrows the output; it does not reduce requests or rows. - Two undocumented inputs are read from the JSON.
sortOrderis accepted and passed straight to Instagram as the feed tab; the code's default is"ranked"and"recent"is the documented alternative.maxCommentsis accepted and logged, then never used — no comments are fetched by this Actor at any setting. Neither appears in the schema, so neither is settable from the Console.
Example input
{"startUrls": ["https://www.instagram.com/explore/locations/213131048/berlin-germany/","@mrbeast","hamburg"],"maxItems": 60,"sessionId": "51234567890%3AqR7tVbNm2XyZaP%3A17%3AAYd8kQpLxWm","filterKeywords": ["brunch", "coffee", "specialty"],"mediaType": "all","extractEntities": true,"includeTaggedUsers": true,"includeEngagement": true,"includeVideoMetadata": true,"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]},"dateFilterType": "relative","relativeValue": 30,"relativeUnit": "days","absoluteStartDate": "","absoluteEndDate": ""}
⬆️ Output
Typed, normalized JSON with a consistent shape across runs — 24 keys on every row, nested objects nulled rather than dropped. Rows land location by location as each place finishes, so a multi-location run fills the dataset in batches. Export as JSON, CSV or Excel, or read the dataset through the Apify API.
Every row is charged, including the ones your filter rejects. There are no free header rows, no diagnostic rows and no accounting rows in this Actor: both write paths push with the same row_result event, so there is no filter expression that separates charged from uncharged records — because every record is charged. A run that fetches nothing writes nothing at all, which is why a failed run leaves an empty dataset rather than an explanatory row. Filter for what you wanted downstream:
matches = [r for r in items if r["passedFilter"]]
One behaviour to know about the proxy-fallback path: if a location fails on the first attempt and succeeds after the Actor switches proxies, those rows are pushed without the annotation step. They arrive with matchedKeywords: [] and passedFilter: true regardless of your filter, and the four toggles are not applied to them. Select RESIDENTIAL up front so the fallback never triggers, or re-check caption and hashtags yourself on any row whose matchedKeywords is empty while passedFilter is true.
Example output
A carousel post from a Berlin location, matching two of the three filter keywords:
{"type": "post","id": "3712648095471203885","code": "DQ7fLmXkVtR","url": "https://www.instagram.com/p/DQ7fLmXkVtR/","createdAt": "2026-07-21T09:14:02Z","passedFilter": true,"matchedKeywords": ["brunch", "coffee"],"hashtags": ["brunchberlin", "specialtycoffee", "kreuzberg"],"mentions": ["fiveelvet.coffee"],"taggedUsers": [{"username": "fiveelvet.coffee","fullName": "Five Elvet Coffee Roasters","isVerified": false,"position": [0.482, 0.671]}],"likeCount": 1843,"commentCount": 62,"caption": "Sunday brunch done properly, filter from @fiveelvet.coffee and the best sourdough in the city #brunchberlin #specialtycoffee #kreuzberg","isAvailable": true,"isLikeAndViewCountsDisabled": false,"isPinned": false,"isPaidPartnership": false,"isCarousel": true,"isVideo": false,"owner": {"id": "48219073615","username": "berlin.plates","fullName": "Berlin Plates","profilePicUrl": "https://scontent-ber1-1.cdninstagram.com/v/t51.2885-19/419283746_1180_n.jpg","isPrivate": false,"isVerified": false},"location": {"id": "213131048","name": "Berlin, Germany","city": "Berlin","lat": 52.52,"lng": 13.405},"video": null,"image": {"url": "https://scontent-ber1-1.cdninstagram.com/v/t51.2885-15/486213907_18412_n.jpg","width": 1440,"height": 1800},"audio": null}
A reel from the same location that failed the keyword test — note that it is still in the dataset, and still charged:
{"type": "post","id": "3714902776310884521","code": "DRB2kQyTnLp","url": "https://www.instagram.com/p/DRB2kQyTnLp/","createdAt": "2026-07-23T18:40:07Z","passedFilter": false,"matchedKeywords": [],"hashtags": ["berlinnights", "technoclub"],"mentions": [],"taggedUsers": [],"likeCount": 9204,"commentCount": 311,"caption": "3am and nothing else like it #berlinnights #technoclub","isAvailable": true,"isLikeAndViewCountsDisabled": false,"isPinned": false,"isPaidPartnership": false,"isCarousel": false,"isVideo": true,"owner": {"id": "7719340852","username": "mara.kellner","fullName": "Mara Kellner","profilePicUrl": "https://scontent-ber1-2.cdninstagram.com/v/t51.2885-19/430118822_1094_n.jpg","isPrivate": false,"isVerified": true},"location": {"id": "213131048","name": "Berlin, Germany","city": "Berlin","lat": 52.52,"lng": 13.405},"video": {"id": "video_3714902776310884521","url": "https://scontent-ber1-2.cdninstagram.com/o1/v/t16/f2/m86/AQPnR4kL9wZ.mp4","width": 1080,"height": 1920,"duration": 31.466},"image": {"url": "https://scontent-ber1-2.cdninstagram.com/v/t51.2885-15/491007233_19004_n.jpg","width": 1080,"height": 1920},"audio": {"id": "audio_1204483927716608","title": "Nachtstrom","artist": "Fjell","coverArt": "https://scontent-ber1-2.cdninstagram.com/v/t51.2885-15/audio_cover_1204483.jpg","duration": 180000,"audioUrl": "https://scontent-ber1-2.cdninstagram.com/o1/v/t16/f1/m84/AQMdLp2kR8n.m4a"}}
How does it work?
Instagram builds its location grid from a GraphQL response rather than from rendered HTML, and the query is addressed by a document id that changes whenever Instagram ships front-end code. So the Actor starts by loading the place page through your chosen proxy with your sessionid cookie, priming the companion cookies with one homepage request, then pulling the page's JavaScript bundles from Instagram's CDN to re-derive the current document id and scraping a live CSRF token out of the page HTML.
With those in hand it posts to Instagram's GraphQL endpoint and walks the feed 12 posts at a time, following Instagram's own cursor until the feed reports no next page or your maxItems budget is spent. Each post node is reshaped into the same 24-key row, the date filter is applied before anything is stored, and the keyword, media-type and entity annotations are computed locally from data already in hand — no extra requests.
Because it reads structured JSON rather than markup, an Instagram front-end redesign does not change your field names. Only publicly posted content is collected: the session is used to reach the location feed, never to open private accounts.
One caveat to plan for: there is no post-level deduplication anywhere. Posts are not deduplicated across pagination pages, across the locations one keyword expands into, or across repeated startUrls entries — so a fast-moving feed or two overlapping place ids can produce the same id more than once, and each copy is a charged row. Deduplicate on id if that matters to you. Location URLs themselves are deduplicated within a single keyword search or profile scan.
Integrations
Instagram Location Scraper By Keyword & Hashtag Filter is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset.
Calling Instagram Location Scraper By Keyword & Hashtag Filter from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/instagram-location-scraper-by-keyword-hashtag-filter").call(run_input={"startUrls": ["https://www.instagram.com/explore/locations/213131048/berlin-germany/"],"sessionId": "<YOUR_INSTAGRAM_SESSIONID>","filterKeywords": ["brunch", "coffee"],"maxItems": 60,"proxyConfiguration": {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]},})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row["passedFilter"]:print(row["owner"]["username"], row["url"], row["matchedKeywords"])
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request.
No-code tools (n8n, Make, LangChain)
In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; an IF node on passedFilter splits the matches out of the flow. In Make, the Apify module supports run-and-wait, so a weekly sweep of a venue's location feed can feed a Google Sheets or Airtable step directly. In LangChain, wrap the run endpoint as a tool and hand the returned rows straight to the model, since they are already flat JSON. Apify schedules and webhooks handle recurrence and completion triggers, so a recurring geo monitor needs no code of your own.
Is it legal to scrape Instagram location posts?
Scraping publicly posted content is broadly treated as permissible where no private account is accessed, and this Actor returns only posts their authors published publicly at a place. Private accounts, stories and messages are never returned.
This output contains personal data, and a more sensitive kind than a plain post export. owner.username, owner.fullName, owner.id and owner.profilePicUrl identify real people; taggedUsers[].username and taggedUsers[].fullName identify others they tagged; caption and mentions are text those people wrote; url and code link back to an identifiable individual's post. On top of that, location.lat and location.lng are precise coordinates — and precise location tied to a named, identifiable poster is materially more sensitive than either field alone, because together they describe where a specific person physically was and when.
Under GDPR, UK GDPR and CCPA you need a lawful basis before you store or reuse any of it, and the usual obligations follow. Data minimisation: drop location.lat and location.lng first, then owner.username, owner.id and taggedUsers, before you drop caption — coordinates and identity are the fields that turn an aggregate into a personal profile. Retention: keep rows only as long as your stated purpose needs, and delete on a schedule rather than warehousing runs indefinitely. Subject requests: people whose posts you store can ask what you hold, ask for it corrected, and ask for it erased, so keep id and owner.id indexed well enough to actually find and delete their records. Your Instagram sessionid is your own account credential; handle it accordingly.
Consult legal counsel if your use case involves bulk storage of personal data.
❓ Frequently asked questions
What Instagram location post fields does this Actor return?
The five most used are caption, owner.username, location, likeCount and passedFilter. Every row carries 24 keys, including nested owner, location, video, image and audio objects — see the data fields table above for all of them.
Does it require an Instagram account or login?
Yes, and this is the one real prerequisite. Instagram serves location feeds only to a logged-in session, so you must supply a sessionid cookie from an account you can log into. The schema does not mark the field required, but a run without it returns no posts. The other credential you need is your Apify token.
How many posts can I extract in one run?
maxItems sets the budget and it is run-wide, not per location — 0 means unlimited. Whether you reach it depends on the feed: the Actor requests 12 posts per page and follows Instagram's cursor until the feed reports no next page, so a quiet place simply runs out of posts. The 12-per-page figure is this Actor's request size; the feed's total depth is Instagram's limit, not one this Actor sets.
What happens if a location has no posts matching my keyword filter?
The run still finishes successfully, and — this is the part worth internalising — you still get rows, and you are still charged for them. The filter never suppresses a post; it only sets passedFilter: false and leaves matchedKeywords empty. So a location with a hundred posts and no keyword hits produces a hundred charged rows that all read passedFilter: false. If the location has no posts at all, or Instagram returns an empty feed, the Actor logs "No more posts found", writes nothing for that location, and moves to the next one. In code, treat passedFilter as the thing to branch on, and never assume the dataset arrives pre-filtered.
Am I charged for posts that fail the filter?
Yes. Every row written to the dataset is one row_result event, and rows are written before the keyword verdict affects anything. There are no free rows of any kind in this Actor — no headers, no diagnostics, no accounting records. The one filter that does save you money is the date filter, which drops posts before they are pushed.
Can I scrape multiple Instagram locations at once?
Yes. startUrls is a list and entries are processed in order. A location URL is used directly, an @username is expanded into every location found on that profile's page, and a bare keyword is resolved against Instagram's place search into up to 10 locations. All of them draw on the same run-wide maxItems budget, so size it for the whole job rather than for one place.
Does it work with Claude, ChatGPT and other AI agent tools?
Yes. It is callable as a standard HTTP-triggered run through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. Have the agent read passedFilter before it reasons over a row.
How does this compare to other Instagram location scrapers?
Checked on the Apify Store on 25 July 2026, the closest listing is instaprism/instagram-location-filtered, and it solves a different problem in a different way. Its README states it does not require your Instagram login or cookies, takes locationIds (numeric ids, required) with a limit defaulting to 500, and filters on account-level metrics — minFollowers, verifiedOnly, businessOnly, hasEmail, hasWebsite, minPosts. Its documented output is one row per user, with profile fields such as followers, email and engagement rate, plus sourceLocationId and sourcePostUrl; captions, hashtags, tagged users and coordinates do not appear in its output example. It also publishes a processing-time table and says results are saved every 60 seconds. Its sibling instaprism/instagram-hashtag-filtered applies the same account-metric filtering to hashtags instead of places, and documents automatic user deduplication. khadinakbar/instagram-reels-scraper covers reels from profiles, hashtags or reel URLs with maxReelsPerSource defaulting to 20 and states that no login is required; its listing documents no location input at all. Their pricing and speed claims are theirs, not verified here.
Set against that, this Actor's honest position: it does need your own sessionid, and in exchange it returns the post, not a profile summary — caption, parsed hashtags and mentions, tagged users with tag positions, audio metadata, and location.lat / location.lng — with keyword and media-type filtering that annotates rather than removes, and charges for every row it fetches. If you want account-metric filtering and no cookie, the listings above are the ones to compare against.
Which fields work best for LLMs, RAG indexing and AI training data?
Every row is typed, normalized JSON with the same field names across runs — no HTML parsing, no selectors — so you can pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool. For RAG, caption carries by far the most information per record and chunks cleanly, with hashtags, mentions and location.name as ready-made metadata filters. For training data, likeCount, commentCount, createdAt, isVideo and isCarousel are the most structurally consistent fields across records. Remember that likeCount and commentCount are null when includeEngagement is off, that the five nested objects can be null, and that audio.duration is in milliseconds on the original-sound path — impute or check before feeding any of them to a model.
What happens when Instagram changes its layout or anti-bot system?
The scraper is maintained, and because it reads Instagram's GraphQL response rather than rendered markup, a front-end redesign generally does not change your output at all. The query id it depends on is re-derived from Instagram's own JavaScript bundles on every run rather than hardcoded, which is the part that usually breaks in hand-rolled scrapers. Your field names and types stay the same on your end. The failure you are most likely to see is not a layout change but an expired sessionid — that one is on your side to refresh.
Can I use it without managing proxies or browser infrastructure?
Yes. There is no browser to run — the Actor talks to Instagram's JSON endpoint directly — and the proxy ladder is automatic: no proxy, then Apify datacenter, then Apify Residential, with the working residential IP held sticky for the rest of the run. You never create a proxy account or rotate an IP yourself. Selecting RESIDENTIAL in the input is still the recommendation, since Instagram login-walls datacenter addresses and starting there skips a wasted rung. The Actor does not solve CAPTCHAs, and it makes no claim to.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| Instagram Profile Scraper | Follower counts, bio and profile metadata for a username |
| Instagram Hashtag Scraper | Posts and reels from any hashtag feed |
| Instagram Comment Leads Scraper | Comment text and commenter details under a post |
| Instagram Mentions Scraper | Public posts that tag a given account |
| Instagram Reel Creator Insights Scraper | Reel-level metrics plus creator insight fields |
| Instagram Related Person Scraper | Suggested and related accounts for a profile |
💬 Your feedback
Found a bug, or need a field that is in Instagram's location payload but not in the output — every carousel slide, comment text, poster follower counts? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON and the location URL you ran are the fastest to reproduce and fix.