Instagram Location Scraper By Keyword & Hashtag Filter avatar

Instagram Location Scraper By Keyword & Hashtag Filter

Pricing

$19.99/month + usage

Go to Apify Store
Instagram Location Scraper By Keyword & Hashtag Filter

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

Scraper Engine

Maintained by Community

Actor stats

0

Bookmarked

41

Total users

2

Monthly active users

5 days ago

Last modified

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:

  1. Log in to Instagram in your browser
  2. Open DevTools (F12) → ApplicationCookieshttps://www.instagram.com
  3. Copy the value of the sessionid cookie into the sessionId input

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 CategoryReturned by this ActorGated or not extracted
Post media, caption and timestamp✅ Public
Place name, city, latitude and longitude✅ Publiclat / lng are null when Instagram omits them
Poster username, full name, id, verified flag✅ Public
Like and comment counts✅ PublicAuthor can hide them; isLikeAndViewCountsDisabled flags it
Hashtags and @mentions written in the caption✅ Public
People tagged in the media✅ Best effortEmpty when Instagram trims usertags from the feed node
The location feed itselfLogin-walled — needs your sessionid
Comment text, commenter identity, poster follower count and bioNot on the location feed — use a comments or profile Actor
Posts from private accounts, stories, DMsNever 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_result event, whether passedFilter is true or false. 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"]].
  • maxItems counts posts fetched, not posts kept. It is applied while paginating, before the keyword and media-type verdict exists. maxItems: 20 with 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 NameDescription
typeAlways the string post
idInstagram media id, as a string
codeInstagram short code for the post
urlCanonical post URL, built as https://www.instagram.com/p/{code}/
createdAtPublish time as ISO-8601 with a trailing Z; falls back to the Unix epoch when Instagram omits the timestamp
passedFiltertrue when the post satisfies both your keyword test and your media-type test
matchedKeywordsWhich of your filterKeywords were found, in the order you supplied them; [] when you supplied none
hashtagsHashtags parsed from the caption, # stripped, deduplicated, order preserved
mentions@-mentions parsed from the caption, @ stripped, deduplicated, order preserved
taggedUsersPeople tagged in the media — array of {username, fullName, isVerified, position}
likeCountLike count, or null when includeEngagement is off
commentCountComment count, or null when includeEngagement is off
captionFull caption text, "" when the post has none
isAvailableAlways true on a returned row
isLikeAndViewCountsDisabledtrue when the author hid their counts
isPinnedtrue when the post is pinned for users
isPaidPartnershipInstagram's paid-partnership flag
isCarouseltrue for multi-item posts
isVideotrue for videos and reels
ownerPoster object — {id, username, fullName, profilePicUrl, isPrivate, isVerified}, or null
locationPlace object — {id, name, city, lat, lng}, or null
videoVideo object — {id, url, width, height, duration}, or null
imageImage object — {url, width, height}, or null
audioAudio object — {id, title, artist, coverArt, duration, audioUrl}, or null

Nested objects are null rather 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.

  1. Open the Actor on Apify and click Try for free
  2. Paste your Instagram sessionid cookie into Instagram sessionid Cookie
  3. Add one or more entries to Location URLs, Usernames or KeywordsstartUrls is the one schema-required input. A run started without it logs "No startUrls provided" and exits immediately with an empty dataset and no charge
  4. Optionally add terms to Keyword Filter and pick a Media Type to set the passedFilter verdict
  5. Set Post Limit, and set a date window if you want one — remember relativeValue defaults to 0, which means no date filtering
  6. Turn Proxy Configuration on and select RESIDENTIAL; Instagram login-walls datacenter IPs
  7. 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 filterKeywords set to their brand terms, then keeps rows where passedFilter is true and reads owner.username and caption to find customers already posting about them by name.
  • 🗺️ A tourism board analyst measuring landmark seasonality runs the same location with dateFilterType set to absolute across two summers and compares likeCount, commentCount and post volume per location.name between the windows.
  • 🎥 A short-form content strategist sets mediaType to video so only reels earn passedFilter: true, then groups the passing rows by audio.title to see which tracks are carrying reach at that place this month.
  • 🏷️ An influencer marketer building a local roster collects taggedUsers and mentions across 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, hashtags and location.name into a vector store with location.lat and location.lng as 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.

ParameterRequiredTypeDescriptionExample Value
startUrlsYesarrayInstagram 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/"]
maxItemsNointegerMaximum posts to scrape per run before filtering. 0 = unlimited. Minimum 0, default 20.50
sessionIdNostringYour Instagram sessionid cookie value. Required in practice — logged-out location feeds are login-walled and return no posts. Stored encrypted."51234567890%3AqR7tVbNm2XyZaP%3A17"
filterKeywordsNoarrayKeep-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"]
mediaTypeNostringWhich media type counts as a match for passedFilter. Enum: all, image, video, carousel. Default "all"."video"
extractEntitiesNobooleanParse #hashtags and @mentions out of each caption. Turn off to leave both arrays empty. Default true.true
includeTaggedUsersNobooleanCollect people tagged in the media into taggedUsers. Turn off to leave the array empty. Default true.true
includeEngagementNobooleanInclude likeCount and commentCount. Turn off to set both to null. Default true.true
includeVideoMetadataNobooleanInclude video.width, video.height and video.duration. Turn off to keep only video.id and video.url and null the rest. Default true.true
proxyConfigurationNoobjectApify Proxy settings. Prefilled with {"useApifyProxy": false}. RESIDENTIAL is what Instagram effectively needs.{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}
dateFilterTypeNostringDate filter mode. Enum: absolute, relative. Default "relative"."relative"
absoluteStartDateNostringKeep posts on or after this date, YYYY-MM-DD. Used when dateFilterType is absolute. Default ""."2026-05-01"
absoluteEndDateNostringKeep posts on or before this date, YYYY-MM-DD. Used when dateFilterType is absolute. Empty means now. Default ""."2026-07-01"
relativeValueNointegerHow 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
relativeUnitNostringUnit for the relative window. Enum: days, weeks, months, years. Default "days"."days"

Seven honest notes on how these behave:

  • What startUrls actually accepts. An entry is treated as a location URL only if it starts with http:// or https:// 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/213131048 without 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 bare mrbeast and a bare numeric id like 213131048: 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 id 213131048 (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 @username path 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-wide maxItems budget stops it.
  • maxItems has two different defaults. The schema default is 20, which is what the Console sends. If you build the input JSON yourself and omit the key entirely, the code falls back to 100.
  • The four toggles do not save any work or cost. extractEntities, includeTaggedUsers, includeEngagement and includeVideoMetadata are 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. sortOrder is accepted and passed straight to Instagram as the feed tab; the code's default is "ranked" and "recent" is the documented alternative. maxComments is 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 ApifyClient
client = 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.

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 location0 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.

Scraper NameWhat it extracts
Instagram Profile ScraperFollower counts, bio and profile metadata for a username
Instagram Hashtag ScraperPosts and reels from any hashtag feed
Instagram Comment Leads ScraperComment text and commenter details under a post
Instagram Mentions ScraperPublic posts that tag a given account
Instagram Reel Creator Insights ScraperReel-level metrics plus creator insight fields
Instagram Related Person ScraperSuggested 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.