# Instagram Scraper - Profiles, Posts, Reels, Hashtags, Places (`snow_leo_data/instagram-scraper`) Actor

Gets 200 posts per account past Instagram's 12-post limit: 200 unique posts in 4 requests, zero repeats. Scrape Instagram profile data and Instagram reel play counts with no login. An empty run names one of 10 reasons instead of a silent empty table.

- **URL**: https://apify.com/snow\_leo\_data/instagram-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Social media, For creators, Automation
- **Stats:** 7 total users, 3 monthly users, 97.6% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 profiles

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Instagram Scraper — profiles, posts, reels, hashtags and places

Public Instagram data without a login, without cookies and without a browser.
One table holds all four kinds of row: **profiles**, **posts and reels**,
**hashtags** and **places**.

The point of this actor is the post walk. Instagram's own page stops at
**12 posts** per account for a logged-out visitor. This actor keeps going
through the same feed query the site itself uses: **50 posts per request,
cursor after cursor**. Measured on 2026-09-17 on @nasa: **200 unique posts
over 4 requests, zero repeats**, reaching from 2026-09-16 back to 2026-04-10.

### What you get

**Profile** — username, user id, full name, biography, followers, following,
post count, verified, private, memorialised, profile picture, every bio link
with its title, external URL, pronouns, account badges, Threads username,
highlight count, whether the account has reels.

**Post / reel** — shortcode, post id, type (photo, video, carousel, reel),
product type, date, likes, comments, plays, caption, hashtags, mentions, owner
username, owner id, owner full name, owner verified, image URL, video URL,
width, height, carousel item count and every carousel image, tagged users,
co-authors, paid-partnership flag, comments-disabled flag, alt text, reel
music title and artist, Instagram's topic pills, place name and coordinates.

**Hashtag** — the tag, its volume when Instagram states it, Instagram's own
description of the tag, related keywords, plus the 12 posts of that page.

**Place** — id, name, latitude, longitude, plus the 18 posts of that page,
each with likes, comments and dates.

### What Instagram does not give a logged-out visitor

Written plainly, because you will hit these and should know beforehand:

- **Comments.** No comment text is served without a login.
- **Followers and following lists.** Only the counts.
- **Business category, contact e-mail and phone.** Not in the logged-out page.
- **Stories and highlights content.** Only the highlight count.
- **Hashtag coverage is partial.** Instagram serves its logged-out hashtag
  page for some tags and the login wall for others: of 5 tags tried on
  2026-09-17, 2 (`travel`, `nature`) returned 12 posts each and 3 (`nasa`,
  `food`, `photography`) returned the login wall. Closed tags are named in the
  run report; they do not fail the run. Tag pages also cannot be paged: the
  pagination query answers with null to a logged-out caller.
- **Hashtag volume is often missing.** `travel` reports 3.9B, `nature`
  reports nothing at all. An absent number is `null`, never a guess.
- **Place pages stop at 18 posts.** Their pagination query answers 401.
- **Play counts exist only for reels on the account's reels tab** (up to 12
  per account, switch on *Add play counts for recent reels*). Feed posts and
  post pages carry no view count at all.
- **Private and withheld accounts.** Instagram answers with an error shell;
  the run report names the account and the reason instead of guessing.

### How much of it costs what

| Row kind | How it is fetched | Wire traffic |
|---|---|---|
| Profile | one page | 136–164 KB (141 KB typical) |
| Post / reel from an account | GraphQL feed, 50 per call | 4.6 KB per post |
| Hashtag + its 12 posts | one page | 157 KB |
| Place + its 18 posts | one page | 333 KB |
| Post opened for place, music, photo tags | one page per post | 149 KB |

Pages are requested with gzip, which is why a profile page costs 141 KB
instead of the 702 KB it weighs unpacked.

### Watch mode

`onlyNew` remembers what earlier runs delivered and returns only what is new
or changed, each row marked `NEW` or `UPDATED` in `change_type`. Signed CDN
URLs change on every view, so they are deliberately left out of the change
fingerprint — otherwise every post would look updated every morning and you
would pay for the whole table again. A run that finds nothing new returns an
empty dataset and says `nothing-changed` in the report.

### Filters

`minLikes`, `minComments`, `minFollowers`, `postedAfter`, `postedBefore`,
`captionContains`, `postTypes`. Two rules worth knowing:

- Filters run **before** billing, so rows they remove are not charged.
- A post whose owner hides like counts arrives with `likes: null` and is
  **kept** by `minLikes` — missing data is not a mismatch. The single
  exception is `captionContains`: a post with no caption cannot contain your
  phrase, so it is dropped.
- `postedAfter` also **stops the crawl**, because the feed runs newest first.
  That is traffic you do not buy, not just rows you do not see.

`maxItems` caps the rows written to the dataset and never the crawl: with
filters on, the crawl deliberately goes deeper so that a filtered run does not
hand you an empty table.

### An empty dataset always says why

`no-input`, `nothing-requested`, `date-range-inverted`,
`follower-filter-without-profiles`, `charge-limit-reached`, `nothing-changed`,
`filtered-out`, `targets-not-available`, `nothing-collected`,
`source-returned-nothing` — one of these lands in `empty_reason` in the run
report, and in the log as a warning. The first four are decided by your input
alone and stop the run before a single request is made.

### Proxy

Instagram blocks Apify's cloud addresses by name: measured 2026-09-17, a
direct profile request from the cloud returns 200 and a 499 KB page with not
one field in it, while the same page through a residential address returns the
real data. The proxy input is therefore preset to **residential**, and you can
change it like in any Apify actor. The run report tells you how many requests
were made and how many bytes they cost.

### Output

One flat table with a `result_type` column (`profile`, `post`, `hashtag`,
`location`). `compactOutput` trims it to 18 columns for AI agents and
spreadsheets; `excludeEmptyFields` drops nulls, empty lists and false values.
The run report (`REPORT` in the key-value store) carries rows per kind, what
each filter removed, which targets Instagram refused, the traffic bought and
`empty_reason`.

### What buyers of the big Instagram actors are asking for

Read on 2026-09-17 off the public issue boards of the seven largest Instagram
actors on Apify — 156 open and closed tickets from people who already pay.
Below is what they ask for and where this actor stands. Nothing here is a
promise about their software; it is what their own buyers wrote.

| What the tickets say | Here |
|---|---|
| "Post count came as empty for some accounts which actually do have posts", "Not retrieving count of posts for some instagram pages" — 2 tickets | `posts_count` is read out of the `og:description` line, because the page JSON carries `all_media_count: null` on every account measured |
| "Number of posts downloaded exceeds 12", "Returns only a few posts", "Detailed mode — not all posts returned", "Not retrieving all posts from date" — 4 tickets | 50 posts per feed call, cursor after cursor: **200 unique posts over 4 requests, zero repeats** (measured 2026-09-17 on @nasa), up to 5000 per account |
| "videoPlayCount is null", "No views returned", "I do not see any Number of Views data", "Add videoPlayCount to the latest posts output" and nine more — 15 tickets | `plays` comes from the reels tab, the one logged-out surface that carries `play_count`. Switch: *Add play counts for recent reels*. Honest limit: Instagram lists about 12 reels there per account, and feed posts carry no view count at all |
| "Run marked as Succeeded but returns empty latestPosts", "empty successful datasets", "Common transient no items" — 7 tickets | an empty dataset always carries one of **10 named reasons** in `empty_reason`; four of them stop the run before the first request |
| "restricted\_page conflates three different situations — two of them carry no distinguishing field" | every target Instagram refused is listed in the run report by name and reason, and does not fail the run |
| "Usernames containing `.com` resolve to a different account's profile (greedy `.com` stripping)" | `nike.com` stays `nike.com`; only `@`, slashes and the query string are stripped |
| "Duplicated rows! The account has 68k followers, you extracted 118k" | the post walk is cursor-based and de-duplicated by shortcode: zero repeats in the 200-post measurement |
| "Expose coauthor\_producers so Instagram collaboration posts are detectable" | `coauthors` ships on every post row |
| "paidPartnership is not returned" | `is_paid_partnership` ships on every post row |
| "Add profile picture (favicon/avatar) to the response" | `profile_pic_url` ships on every profile row |
| "Date filtered results unexpected", "Not retrieving all posts from date" — 3 tickets | `postedAfter` and `postedBefore` filter before billing, and `postedAfter` also stops the crawl, since the feed runs newest first |
| "Add maxRequestRetries / per-profile timeout — one blocked profile gates the whole run" | a profile that fails is reported and skipped; the run continues. The per-request retry count is fixed at 3 and is **not** an input yet |
| "Business address not returned for business accounts that display address on profile", "Not showing mobile number and email" — 3 tickets | not closed, and not promised: the logged-out business profile page carries no address, phone or e-mail (checked on @gopro) |
| "Mentions limited to 21 results", "Prefiltering Comments", "Instagram Search Scraper blocked" | not closed: mentions, comment text and keyword search are all behind the login wall |

### Defaults

Running with the preset input returns the @nasa profile row and its 12 most
recent posts. `maxItems` defaults to 50 so a trial run stays cheap;
`postsPerProfile` defaults to 12 and goes up to 5000.

# Actor input Schema

## `usernames` (type: `array`):

Usernames or profile URLs: nasa, @nasa, https://www.instagram.com/nasa/. Each one returns a profile row plus as many of its posts as you ask for below.

## `postsPerProfile` (type: `integer`):

How many posts and reels to return for every account. Posts come from Instagram's own logged-out feed, 50 per request, page after page — measured 200 posts over four requests with no repeats, reaching five months back. 0 returns profile rows only.

## `includeProfiles` (type: `boolean`):

Followers, following, post count, bio, bio links, verified and private flags for every account in the list.

## `postUrls` (type: `array`):

Post or reel URLs (https://www.instagram.com/p/XXXX/ or /reel/XXXX/), or bare shortcodes. These are read from the post page, which also carries the place, the tagged users and the reel's music.

## `hashtags` (type: `array`):

Hashtags without the # sign. Instagram serves its logged-out hashtag page for part of its tags only: of five tags measured on 17.09.2026, travel and nature returned 12 posts each, while nasa, food and photography answered with the login wall. Tags that are closed are named in the run report instead of failing the run.

## `includeHashtagPosts` (type: `boolean`):

On top of the hashtag row itself (volume, description, related keywords), return the 12 posts Instagram shows on that page.

## `locationUrls` (type: `array`):

Location URLs (https://www.instagram.com/explore/locations/212988663/new-york-new-york/) or bare location ids. One place page returns 18 posts with full engagement data.

## `includeRelatedAccounts` (type: `boolean`):

Instagram's logged-out feed mixes in posts of accounts tagged in, or co-authoring with, the one you asked for: measured 131 posts of @nasa inside 200 returned. Off by default, so you get only the account you asked for.

## `includeReelPlays` (type: `boolean`):

Fetches the account's reels tab, the only logged-out page that carries play\_count, and fills the plays column for the reels found there (up to 12 per account). One extra page per account.

## `includePostDetails` (type: `boolean`):

Opens the page of each post to add the place with coordinates, the reel's music, the users tagged inside the photo and Instagram's topic pills. This is one page per post (about 150 KB) against 4.7 KB per post in the feed, so it is charged as a separate event.

## `onlyNew` (type: `boolean`):

Remembers what earlier runs delivered and returns only new and updated items, each marked NEW or UPDATED in change\_type. A run that finds nothing new returns an empty dataset and says so in the report.

## `emitUnchanged` (type: `boolean`):

In watch mode, also return items that did not change, marked UNCHANGED.

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

Hard cap on rows written to the dataset — and only on rows written, never on the crawl. With filters on, the crawl goes deeper than this number so the filters do not empty your table. Empty means 50, which keeps a trial run cheap.

## `minLikes` (type: `integer`):

Drops posts below this many likes. Posts whose owner hides like counts arrive with likes = null and are kept, because missing data is not a mismatch.

## `minComments` (type: `integer`):

Drops posts below this many comments. Posts where Instagram hides the count are kept.

## `minFollowers` (type: `integer`):

Applies to profile rows only; post rows pass through untouched.

## `postedAfter` (type: `string`):

YYYY-MM-DD. The feed is walked newest first, so this also stops the crawl instead of only filtering it — that is your money saved, not just your table shortened.

## `postedBefore` (type: `string`):

YYYY-MM-DD.

## `captionContains` (type: `string`):

Case-insensitive substring of the caption. Posts with no caption at all are dropped by this filter.

## `postTypes` (type: `array`):

Leave empty for everything.

## `compactOutput` (type: `boolean`):

18 columns instead of the full set — for AI agents and spreadsheets.

## `excludeEmptyFields` (type: `boolean`):

Leaves out null, empty list and false values from every row.

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

Instagram blocks Apify's own cloud addresses by name: measured 17.09.2026, a direct profile request from the cloud returns 200 and 499 KB with not a single field in it, while the same page through a residential address returns the real data. Residential is therefore the default. Pages are requested with gzip, which puts one profile page at about 144 KB on the wire instead of 719 KB.

## Actor input object example

```json
{
  "usernames": [
    "nasa"
  ],
  "postsPerProfile": 12,
  "includeProfiles": true,
  "postUrls": [],
  "hashtags": [],
  "includeHashtagPosts": true,
  "locationUrls": [],
  "includeRelatedAccounts": false,
  "includeReelPlays": false,
  "includePostDetails": false,
  "onlyNew": false,
  "emitUnchanged": false,
  "maxItems": 50,
  "postTypes": [],
  "compactOutput": false,
  "excludeEmptyFields": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All collected rows

# 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 = {
    "usernames": [
        "nasa"
    ],
    "postsPerProfile": 12,
    "postUrls": [],
    "hashtags": [],
    "locationUrls": [],
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/instagram-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 = {
    "usernames": ["nasa"],
    "postsPerProfile": 12,
    "postUrls": [],
    "hashtags": [],
    "locationUrls": [],
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

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

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

```

## CLI example

```bash
echo '{
  "usernames": [
    "nasa"
  ],
  "postsPerProfile": 12,
  "postUrls": [],
  "hashtags": [],
  "locationUrls": [],
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call snow_leo_data/instagram-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/instagram-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/M8sVC0sbgawX88AYh/builds/bLfjRKcqeGGMJikfu/openapi.json
