# Instagram Location Scraper By Keyword & Hashtag Filter (`scraper-engine/instagram-location-scraper`) Actor

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.

- **URL**: https://apify.com/scraper-engine/instagram-location-scraper.md
- **Developed by:** [Scraper Engine](https://apify.com/scraper-engine) (community)
- **Categories:** Social media, Agents, Automation
- **Stats:** 41 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

$19.99/month + usage

To use this Actor, you pay a monthly rental fee to the developer. The rent is subtracted from your prepaid usage every month after the free trial period.You also pay for the Apify platform usage, which gets cheaper the higher Apify subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#rental-actors

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### 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) → **Application** → **Cookies** → `https://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 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_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 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 `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 Keywords** — `startUrls` 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.

| 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 `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

```json
{
  "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:

```python
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:

```json
{
  "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:

```json
{
  "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

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

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

# Actor input Schema

## `startUrls` (type: `array`):

Instagram location URLs, @usernames, or place keywords — input type is auto-detected. Example: 'https://www.instagram.com/explore/locations/213131048/berlin-germany/', 'mrbeast', or 'berlin'.

## `maxItems` (type: `integer`):

Maximum posts to scrape per run before filtering (0 = unlimited).

## `sessionId` (type: `string`):

Your Instagram 'sessionid' cookie value. Required in practice — logged-out location feeds are login-walled and return no posts. Copy it from your browser cookies while logged into instagram.com. Stored encrypted.

## `filterKeywords` (type: `array`):

Keep only posts whose caption, hashtags, @mentions or tagged-user names contain any of these terms (case-insensitive). Leave empty to keep every post. Matches are listed per post in 'matchedKeywords' and drive the 'passedFilter' flag.

## `mediaType` (type: `string`):

Which media types count as a match for 'passedFilter': all, image only, video/reel only, or carousel (multi-item) only.

## `extractEntities` (type: `boolean`):

Parse #hashtags and @mentions out of each caption into 'hashtags\[]' and 'mentions\[]'. Turn off to leave those arrays empty.

## `includeTaggedUsers` (type: `boolean`):

Collect people tagged in the media (username, full name, verified, tag position) into 'taggedUsers\[]' from the feed node. Best-effort: empty when Instagram omits tags from the feed response.

## `includeEngagement` (type: `boolean`):

Include likeCount and commentCount on each post. Turn off to withhold them (set to null).

## `includeVideoMetadata` (type: `boolean`):

Include detailed video metadata (width, height, duration). Turn off to keep only the video id and url.

## `proxyConfiguration` (type: `object`):

Proxy to route requests through. If Instagram rejects it, a residential proxy is used as fallback.

## `dateFilterType` (type: `string`):

Filter posts by absolute date range or a relative window.

## `absoluteStartDate` (type: `string`):

Only keep posts on/after this date. Used when Date Filter Mode = absolute.

## `absoluteEndDate` (type: `string`):

Only keep posts on/before this date. Used when Date Filter Mode = absolute. Empty = now.

## `relativeValue` (type: `integer`):

How many time units back to keep posts from. Used when Date Filter Mode = relative. 0 = no date filtering.

## `relativeUnit` (type: `string`):

Unit for the relative window.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.instagram.com/explore/locations/213131048/berlin-germany/"
  ],
  "maxItems": 20,
  "filterKeywords": [],
  "mediaType": "all",
  "extractEntities": true,
  "includeTaggedUsers": true,
  "includeEngagement": true,
  "includeVideoMetadata": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "dateFilterType": "relative",
  "absoluteStartDate": "",
  "absoluteEndDate": "",
  "relativeValue": 0,
  "relativeUnit": "days"
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        "https://www.instagram.com/explore/locations/213131048/berlin-germany/"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    },
    "relativeValue": 0
};

// Run the Actor and wait for it to finish
const run = await client.actor("scraper-engine/instagram-location-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": ["https://www.instagram.com/explore/locations/213131048/berlin-germany/"],
    "proxyConfiguration": { "useApifyProxy": False },
    "relativeValue": 0,
}

# Run the Actor and wait for it to finish
run = client.actor("scraper-engine/instagram-location-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://www.instagram.com/explore/locations/213131048/berlin-germany/"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "relativeValue": 0
}' |
apify call scraper-engine/instagram-location-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=scraper-engine/instagram-location-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/uT8AbM1ommxYRjgnE/builds/hjKYViHrad7Z4swZZ/openapi.json
