# Instagram Scraper: Posts, Reels, Comments & Hashtags (`abdelrahman12_3/instagram-scraper`) Actor

Public Instagram data without login. Flat low prices, no charge for empty or duplicate results, free filters, monitoring mode and a coverage report for every input.

- **URL**: https://apify.com/abdelrahman12\_3/instagram-scraper.md
- **Developed by:** [abdelrahman hatem](https://apify.com/abdelrahman12_3) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 2 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.36 / 1,000 posts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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, Comments, Hashtags & Locations

Scrape public Instagram data **without logging in**: profiles, posts, reels, comments, hashtags, keywords in any language, and locations. Put anything in one box: usernames, profile links, post links, `#hashtags`, keywords or location links. The scraper works out what each one is.

Two things make it different:

- **You only pay for real results.** You are never charged for empty results, duplicates, posts your filters removed, or posts a monitor already gave you. Filters are free.
- **Every input gets a coverage report.** It says how many items you asked for, how many you got, and exactly why it stopped.

> This Actor is not affiliated with, endorsed by, or sponsored by Instagram or Meta. It only reads data that Instagram shows publicly to logged-out visitors.

### What you can scrape

| Input | Example | What you get |
|---|---|---|
| Profile | `natgeo`, `@natgeo`, `https://www.instagram.com/natgeo/` | Profile details plus the latest posts **and** reels (up to ~24, merged and de-duplicated), and optionally older posts found through Google |
| Post or reel | `https://www.instagram.com/p/DdG4RIxIPyf/` | Full post details plus up to ~15 comments |
| Hashtag | `#travel` | ~12 top posts per hashtag, its related keywords, and a topic description |
| Keyword (any language) | `cairo food`, `مصر`, `keyword:travel` | Same as hashtags |
| Location | `https://www.instagram.com/explore/locations/212988663/new-york-new-york/` or `location:212988663` | Location details plus top and recent posts (up to ~48) |

Paste inputs however you have them: one per line, several on one line separated by commas, with or without `@`, `#` or quotes, or as links copied from the app (share links, story links and search links work too). Duplicates such as `natgeo`, `@NatGeo` and the profile URL are scraped once.

**Need more posts on a topic?** Turn on **Related keyword expansion**. Each related keyword adds about 12 more posts on the same topic, so one hashtag can return hundreds of posts.

### What Instagram shows logged-out visitors

Instagram limits what anyone can see without an account, and every logged-out scraper hits the same limits. Here they are up front:

- **Profiles:** the latest ~12 posts and ~12 reels. Older posts need a login.
- **Comments:** about 15 per post.
- **Hashtags and keywords:** about 12 posts per keyword. Use related keyword expansion to get more.
- **Locations:** about 24 top posts and 24 recent posts.
- **Not available without login:** follower lists, stories, private accounts, and tagged-posts tabs.

When an input stops because of one of these limits, its coverage row says `INSTAGRAM_LOGGED_OUT_CAP`, so you know nothing went wrong.

**Want older posts?** Turn on **Older posts from Google (beta)**. It searches Google for the profile's older posts and opens each one for full details. Only the profile's own posts are kept, and posts by other accounts are dropped without charge. How many you get depends on how much of the profile Google has indexed. In our tests with six searches that was 57 older posts (2020 to 2026) for a large brand, 11 for a mid-size creator, and none for an account with a very common name, where searching stopped after one search. It adds older posts on top of the latest ones, not the complete history.

**Want to build a complete history from now on?** Use **monitoring mode** on a schedule. Each run adds the newest posts, so your dataset builds the full history over time, and you never pay twice for the same post. With monitoring on, each profile is searched on Google at most once every 30 days.

### Monitoring mode

1. Set **Monitor name** (for example `competitors`).
2. Save the input as a task and give it a schedule (hourly, daily…).
3. Each run returns only posts this monitor has never delivered before.

Each input checked costs one *monitor check* event. That covers the page load even when nothing new was posted. New posts are charged as usual.

### Output

Each record type gets its own table, so every table stays clean: **Posts** (the main dataset), **Profiles**, **Comments**, **Keywords**, **Locations** and **Coverage report**. You can open each one from the run's Output tab or through the API. Records in a table always have the same fields; a field that is unknown comes back as `null` instead of disappearing.

#### Post

```json
{
  "recordType": "post",
  "shortcode": "DdG4RIxIPyf",
  "url": "https://www.instagram.com/reel/DdG4RIxIPyf/",
  "type": "reel",
  "detailLevel": "full",
  "caption": "Presented by @Rolex. Welcome to Africa…",
  "hashtags": ["rolex", "perpetualplanet"],
  "mentions": ["rolex", "wunmimosaku", "disneyplus", "hulu"],
  "takenAt": "2026-09-10T13:00:14.000Z",
  "owner": { "id": "787132", "username": "natgeo", "fullName": "National Geographic", "isVerified": true },
  "likesCount": 78080,
  "likesHidden": false,
  "commentsCount": 516,
  "viewsCount": null,
  "imageUrl": "https://…",
  "videoUrl": "https://…",
  "carousel": [],
  "taggedUsers": [{ "username": "rolex", "isVerified": true }],
  "coauthors": [{ "username": "disneyplus" }],
  "music": { "title": "Original audio", "artist": "natgeo", "isOriginalAudio": true },
  "metrics": { "engagementRate": 0.00029, "ageHours": 533.5, "viewsPerHour": null },
  "source": { "kind": "post", "value": "DdG4RIxIPyf" },
  "input": "https://www.instagram.com/p/DdG4RIxIPyf/"
}
```

**`detailLevel`** tells you how complete a post record is:

- **`basic`:** the post came from a feed grid (shortcode, caption, image, type, and a date read from the accessibility text).
- **`standard`:** the feed also gave counts (reels tab, location feeds).
- **`full`:** the post's own page was opened (exact time, counts, carousel, music, tags, coauthors, location).
- **`partial`:** Instagram withheld the full page, so the scraper used the public embed page instead.

**Hidden likes** come back as `likesCount: null` with `likesHidden: true`, never as `-1`.

#### Profile

The profile record has the user ID, username, name, bio, bio links, follower and following counts, and post count (marked as approximate when Instagram rounds it, e.g. "32K"). It also has verified, private, has-reels and active-story flags, the linked Threads username, and the profile picture.

It also includes computed **metrics** from the visible posts: average likes, comments and views, engagement rate, and posts per week.

#### Comment

Each comment has its text, likes, reply count, date and post link. **Commenter usernames are hidden by default** for privacy: each commenter gets a stable anonymous `ownerHash` instead. Turn on *Include commenter usernames* if you need them and have a lawful reason to process them.

#### Coverage report (free)

The **Coverage report** table has one row per input. The same rows are also saved as the `COVERAGE` record in the run's key-value store.

```json
{
  "input": "natgeo",
  "detectedType": "profile",
  "requested": 100,
  "delivered": { "posts": 17, "profiles": 1, "comments": 0, "keywords": 0, "locations": 0 },
  "skipped": { "filtered": 0, "duplicate": 0, "seenBefore": 0 },
  "stopCode": "INSTAGRAM_LOGGED_OUT_CAP",
  "message": "Delivered everything Instagram shows to logged-out visitors here; more exists but needs a login.",
  "charged": { "post": 17, "profile": 1 }
}
```

The possible stop codes are:

- `LIMIT_REACHED`
- `NO_MORE_DATA`
- `INSTAGRAM_LOGGED_OUT_CAP`
- `PRIVATE_ACCOUNT`
- `NOT_FOUND`
- `LOGIN_REQUIRED`
- `BLOCKED`
- `LAYOUT_CHANGED`
- `SPENDING_LIMIT`
- `RUN_LIMIT`
- `INVALID_INPUT`

The `SUMMARY` record has the run totals and every event charged.

### Pricing

Pay per event. No monthly rental, no minimum results per query, and no extra charge for filters, sorting or country targeting.

| Event | Price per 1,000 |
|---|---|
| Post | $0.45 |
| Profile | $2.20 |
| Full post details (extra, when a post page is opened) | $1.80 |
| Comment | $0.30 |
| Monitor check (per input, monitoring mode only) | $3.00 |
| Saved media file | $2.00 |

Posts, comments and saved media files cost less on higher Apify plans. Each run also has a start fee of $0.00005, which is practically free.

**Example:** 100 profiles with their latest posts (about 2,400 posts) cost about **$1.30**.

**Never charged:**

- empty results
- duplicates
- filtered posts
- posts a monitor already delivered
- errors
- keyword and location records
- coverage rows

Set *Maximum cost per run* when you start a run, and the scraper will stop cleanly when it reaches it.

### Tips

- **Use residential proxies (the default).** Instagram blocks datacenter IPs.
- **Turn on *Full post details* for exact timestamps.** The same goes for likes on photo posts from profiles, and for comment counts. Grid-only records often lack them.
- **Filters on likes or views drop posts whose counts are unknown or hidden.** Turn on full post details if you filter photo posts by likes.
- **Date filters take a date or a relative value.** Use the date picker in the form. Through the API you can send `2026-09-01`, `7 days`, `7d`, `2 weeks ago`, `3 months` or `yesterday`. If a date cannot be read safely, for example `03/04/2026`, the run stops before any request and tells you how to write it. Nothing is charged in that case.
- **Comments need each post's page,** so turning on comments also turns on full post details.
- **Use *Flat output* and snake\_case for spreadsheets and databases.**
- **Use *Run tag* to label batches when you run the same task many times.**

### Legal

This scraper only collects data Instagram shows publicly to logged-out visitors. It never logs in, never uses accounts or cookies, and never bypasses privacy settings.

Some results, such as names, bios and comments, can still be personal data under laws like the GDPR. You are responsible for having a lawful basis to collect and use it, and for collecting only what you need. If you are unsure, ask a lawyer.

### Coming soon

- **AI transcripts:** transcripts for reels.

# Changelog

This Actor's version history is a separate document: https://apify.com/abdelrahman12\_3/instagram-scraper/changelog.md

# Actor input Schema

## `targets` (type: `array`):

One per line (or several separated by commas). Accepts usernames (natgeo or @natgeo), profile URLs, post and reel URLs, #hashtags, keywords in any language, and location URLs. Prefix with "keyword:" to force a keyword or "location:" with a numeric location ID. Duplicates are scraped once.

## `profileDetails` (type: `boolean`):

Followers, following, bio, links, verification and computed engagement for each profile.

## `profilePosts` (type: `boolean`):

The latest posts and reels Instagram shows on a profile to logged-out visitors (up to about 24 per profile).

## `deepHistory` (type: `boolean`):

Finds a profile's older posts through Google image search and opens each one for full details. Only the profile's own posts are kept; posts by other accounts are dropped and never charged. Found posts are charged like any post with full details. Google does not index every post, so this adds older posts rather than the complete history.

## `deepHistorySearches` (type: `integer`):

Upper limit on Google searches for each profile when "Older posts from Google" is on. Each search finds up to about 30 posts. Searching stops early when Google runs out of results or when most results belong to other accounts.

## `postDetails` (type: `boolean`):

Opens every post's own page for exact timestamp, likes, comment count, carousel items, music, tagged users, coauthors and location. Post URLs in "What to scrape" always get full details. Charged as an extra event per post.

## `comments` (type: `boolean`):

Comments Instagram shows on the post page to logged-out visitors (about 15 per post). Comments come from each post's own page, so this also turns on "Full post details" (charged per post).

## `keywordExpansionDepth` (type: `integer`):

For hashtags and keywords: 0 = only your keyword, 1 = also its related keywords, 2 = related keywords of related keywords. Each keyword adds about 12 posts on the topic.

## `maxKeywords` (type: `integer`):

Upper limit on extra keywords explored for each hashtag or keyword input.

## `locationTabs` (type: `string`):

Which location feed to read.

## `newerThan` (type: `string`):

Only posts from this date on. Pick a date, or switch to relative for values like 7 days or 3 months. Leave empty for no limit. Free.

## `olderThan` (type: `string`):

Only posts before this date. Pick a date, or switch to relative (1 day = skip the last 24 hours). Leave empty for no limit.

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

Posts with hidden or unknown likes are excluded when this is set. Turn on full post details for photo posts from profiles.

## `minViews` (type: `integer`):

Applies to videos and reels.

## `mediaTypes` (type: `array`):

Leave empty for all types.

## `skipPinned` (type: `boolean`):

Where Instagram marks posts as pinned.

## `maxItemsPerTarget` (type: `integer`):

Stop collecting posts for an input after this many.

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

Stop the whole run after this many posts.

## `monitorName` (type: `string`):

Turns on monitoring: the run returns only posts this monitor has not delivered before, and you are never charged twice for the same post. Use the same name on a scheduled task. Upper or lower case and extra spaces do not matter.

## `includeCommenterProfiles` (type: `boolean`):

Off by default for privacy: commenters get a stable anonymous ID instead of their username.

## `fieldCase` (type: `string`):

camelCase or snake\_case keys.

## `flatten` (type: `boolean`):

Turn nested objects into dotted keys (owner.username). Handy for spreadsheets.

## `runTag` (type: `string`):

Free text copied onto every record, to label batches.

## `saveMedia` (type: `string`):

Copies images (and optionally videos) into this run's storage before Instagram's links expire. Charged per file.

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

Instagram blocks datacenter IPs. Residential proxies are strongly recommended.

## `maxConcurrency` (type: `integer`):

How many pages are fetched at the same time. Higher is faster but more likely to be blocked.

## `maxRequestsPerMinute` (type: `integer`):

Upper limit on page requests per minute across the whole run.

## Actor input object example

```json
{
  "targets": [
    "natgeo"
  ],
  "profileDetails": true,
  "profilePosts": true,
  "deepHistory": false,
  "deepHistorySearches": 8,
  "postDetails": false,
  "comments": false,
  "keywordExpansionDepth": 0,
  "maxKeywords": 20,
  "locationTabs": "both",
  "mediaTypes": [],
  "skipPinned": false,
  "maxItemsPerTarget": 100,
  "maxItems": 1000,
  "includeCommenterProfiles": false,
  "fieldCase": "camel",
  "flatten": false,
  "saveMedia": "none",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxConcurrency": 8,
  "maxRequestsPerMinute": 120
}
```

# Actor output Schema

## `posts` (type: `string`):

No description

## `profiles` (type: `string`):

No description

## `comments` (type: `string`):

No description

## `keywords` (type: `string`):

No description

## `locations` (type: `string`):

No description

## `coverage` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "targets": [
        "natgeo"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abdelrahman12_3/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 = {
    "targets": ["natgeo"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abdelrahman12_3/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 '{
  "targets": [
    "natgeo"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call abdelrahman12_3/instagram-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abdelrahman12_3/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/Pkqpw9eROpd4YePIT/builds/JEwisA8mNgg0ZsmPK/openapi.json
