# Instagram Scraper (`omaraw/instagram-scraper`) Actor

Scrape Instagram profiles, posts, reels and comments from any mix of profile and post URLs, or from a search term. $2.025 per 1,000 results: 25% below the Free plan price of apify/instagram-scraper, and the same flat rate on every plan.

- **URL**: https://apify.com/omaraw/instagram-scraper.md
- **Developed by:** [itnlab](https://apify.com/omaraw) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.75 / 1,000 results

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

Scrape **Instagram profiles, posts, reels and comments** from any mix of profile and post URLs — followers, engagement averages, captions, like and comment counts, media URLs — at a flat **$1.75 per 1,000 results**, whatever your Apify plan.

### What you get

- **Profiles** — followers, follows, post count, verification, business category, engagement averages computed over the posts in the snapshot, and since 14 September 2026 `biography`, `profilePicUrl`, `externalUrl`/`externalUrls` and `highlightReelCount`.
- **Posts and reels** — caption, timestamp, likes, comments, video views, and media URLs on request. Since 3 September 2026 also `hashtags` and `mentions` cut out of the caption, `taggedUsers` for the accounts tagged *inside* the picture, and `dimensionsWidth`/`dimensionsHeight` for the frame it was published in; since 14 September 2026 `alt`, `productType`, `locationName`/`locationId`, `childPosts` for a carousel's slides, `originalWidth`/`originalHeight`, and `firstComment`/`latestComments`, which arrive in the same response as the post and cost nothing extra.
- **Comments** — one row each, in `apify/instagram-comment-scraper`'s shape.
- **Posts for a keyword** — since 15 September 2026, put a word in **Search keyword** (`search`) and the run collects what Instagram ranks for it, alongside whatever URLs you pasted. Those rows carry `searchTerm` so a mixed run stays attributable.
- **Suggested accounts** — `relatedProfiles`, the accounts Instagram suggests beside a profile. Off by default (`includeRelatedProfiles`): it costs an extra request per profile and the accounts in it are third parties you did not name.
- **A row for every input, including the ones that failed** — a refused or unreachable target writes a row carrying `inputUrl`, `error` and `errorDescription` instead of vanishing. Fifty URLs in means fifty rows out.

### How to use the Instagram scraper

1. Paste profile URLs, handles or single post URLs into **Instagram profiles or posts** (`directUrls`). `https://www.instagram.com/nasa/`, `nasa` and `https://www.instagram.com/p/DCMUrLltVlM/` all work, and one list may mix them.
2. Choose **What to return** (`resultsType`): `details` for the profile alone, `posts` or `reels` for one row per post, `comments` for one row per comment. A post URL answers `posts`, `reels` and `comments` with that one post; `details` asks for an account's own figures, so it is refused for a post URL rather than answered with blanks.
3. Set **Results limit** (`resultsLimit`) — up to 1,000 posts, or up to 5,000 comments per post.
4. Or leave the URLs empty and type a word into **Search keyword** (`search`) with a **Search limit** (`searchLimit`). The two can be combined: a run may do both in one go.
5. Press **Start**. Results land in the dataset as they are collected; export as JSON, CSV, Excel or via the API.

Optional: **Only posts newer than** takes a date (`2026-07-01`) or a span (`3 days`). **Include media URLs** adds image and video links — they are signed by Instagram and expire within hours, so download promptly. **Repeat the profile on every row** carries the owning account onto each post and comment so a flat export can be grouped without keeping state. **Include suggested accounts** (`includeRelatedProfiles`) adds `relatedProfiles` to each profile row.

### How it compares

One flat price for every buyer, 35% below what `apify/instagram-scraper` charges its Free-plan buyers, and a post row that says both how many comments Instagram declares (`commentsCount`) and how many bodies were actually read (`commentsCollected`), so a figure computed over the sample is never mistaken for one over the whole.

Prices below marked *measured* come from runs made on 2 September 2026 against an identical list of public accounts; those marked *published* are the vendor's own figure and were not re-tested here. Rows left as — are outside that Actor's scope rather than a shortcoming.

| Feature | This Actor | apify/instagram-scraper | apify/instagram-profile-scraper | apify/instagram-comment-scraper | apidojo/instagram-scraper |
| ----- | ---- | ----------- | ----------- | ------------- | ------------- |
| Pricing plan | pay per result | pay per result | pay per result | pay per result | pay per result |
| Price per 1,000 profiles or posts | **$1.75** *(flat)* | $2.70 *(Free plan, tiered)* | $2.60 *(Free plan, tiered)* | — | $0.50 *(published)* |
| Price per 1,000 comments | **$1.75** *(flat)* | $2.70 *(Free plan - it charges per result, whatever the row)* | — | $2.60 *(Free plan, tiered)* | — |
| Usable on the Apify free plan | ✔️ | ✔️ | ✔️ | ✔️ | ❌ *(demo only, 10 items)* |
| Profile URLs | ✔️ | ✔️ | ✔️ | ❌ | ✔️ |
| Single post URLs | ✔️ | ✔️ | ❌ | ✔️ | reels URLs *(published)* |
| Keyword search | ✔️ *(`search`, ranked-posts feed)* | ✔️ *(published)* | ❌ | ❌ | ✔️ |
| Hashtag, place, audio URLs | ❌ *(the keyword feed is the reachable approximation)* | hashtag + place *(published)* | ❌ | ❌ | ✔️ |
| An error row for every failed input | ✔️ | ✔️ *(published)* | ✔️ *(published)* | ✔️ *(published)* | ❓ |
| `relatedProfiles` on profile rows | ✔️ *(opt-in)* | ❌ | ✔️ *(published, always on)* | — | ❓ |
| `hashtags`, `mentions`, `taggedUsers`, `dimensions*` on post rows | ✔️ *(tagged accounts minimised)* | ✔️ *(published)* | ✔️ *(published)* | — | ✔️ *(published)* |

### How many results you get

`resultsLimit` is a promise, not a hint: ask for 1,000 posts and a run returns 1,000, or every post the account has when that is fewer. There is no page size to work around and no limit error hiding behind the number you typed. Measured 7 September 2026: 1,000 posts requested from `@nasa`, 1,000 distinct posts returned over 84 pages.

The same holds for comments: ask for 1,000 and a run returns 1,000 where the post has them. Measured 7 September 2026 on `p/Dc6zTucHTv8` (@kimkardashian): three consecutive runs asking for 1,000, three runs returning exactly 1,000.

Where fewer come back it is Instagram's doing rather than this Actor's, and the number to compare against is not `commentsCount`. That is Instagram's own total and it counts **replies and filtered comments a logged-out reader is never served** — the same post declares ~4,350 and its logged-out connection carries ~3,350. This Actor does not take Instagram's first "there are no more" for an answer: the page that announced the end is fetched again before a run reports one, because an unconfirmed end was measured arriving hundreds of comments early. When a walk still ends short, the run log names the post, what was asked for, what came back and what Instagram declares, so a short answer never has to be guessed at.

### Pricing

Pay per result, no platform usage charged on top.

| Result | Price each | Per 1,000 | apify/instagram-scraper, Free plan |
|---|---|---|---|
| Profile | $0.00175 | **$1.75** | $2.70 |
| Post or reel | $0.00175 | **$1.75** | $2.70 |
| Comment | $0.00175 | **$1.75** | $2.70 |

One result is one row whatever its shape, which is the unit that Actor charges on too — and all three rows cost the same here, so a run's bill is its row count and nothing else.

**One flat rate, for everybody.** $1.75 per 1,000 — no plans, no tiers, no volume brackets, and no Apify platform usage charged on top. That is 35% below the $2.70 that `apify/instagram-scraper` charges its Free-plan buyers (read from Apify's public API, 17 September 2026); their rate moves with the subscription a buyer is on, and ours does not move at all.

An answer carrying nothing is free — you pay for rows, not for asking.

### Output example

One post row, as it lands in the dataset:

One row, with the field names and types a real run writes (values are from a sample run, not live figures):

```json
{
  "dataType": "post",
  "id": "3498261938871556428",
  "shortCode": "CmOcK0001aa",
  "type": "Image",
  "url": "https://www.instagram.com/p/CmOcK0001aa/",
  "caption": "New single-origin lot landed this morning: Guji, Ethiopia. Jasmine and stone fruit, roasted light for filter.",
  "timestamp": "2026-08-16T18:01:36.000Z",
  "likesCount": 4310,
  "likesHidden": false,
  "commentsCount": 96,
  "commentsCollected": 10,
  "isVideo": false,
  "hashtags": ["coffee", "singleorigin"],
  "mentions": ["guji.mill"],
  "taggedUsers": [{"username": "guji.mill", "id": "2421050", "isVerified": true, "imageIndex": 0, "position": [0.5439, 0.4595]}],
  "dimensionsWidth": 1440,
  "dimensionsHeight": 1800,
  "ownerUsername": "nasa",
  "ownerId": "17841400000000001",
  "displayUrl": "https://scontent.cdninstagram.com/v/t51.29350-15/a1_n.jpg",
  "images": ["https://scontent.cdninstagram.com/v/t51.29350-15/a1_n.jpg"],
  "mediaAssets": [{"url": "https://scontent.cdninstagram.com/v/t51.29350-15/a1_n.jpg", "width": 1440, "height": 1800, "isVideo": false}],
  "collectedAt": "2026-08-19T18:01:36.000Z",
  "inputUrl": "nasa"
}
```

Field names *and* value shapes follow the conventions the other Instagram scrapers on Apify Store use — `shortCode`, `likesCount`, `ownerUsername`, ids as strings, `type` as `Image`/`Video`/`Sidecar`, timestamps as `2026-08-16T18:01:36.000Z`, and a hidden like count as `-1` rather than `0`, because zero likes is a real answer and "Instagram will not say" is not. An input object written for one of those can usually be pasted straight in; `addParentData` defaults to `false` here for the same reason.

**`images` is a list of URL strings**, which is what `apify/instagram-scraper` writes into a column of that name — checked against its own dataset schema on 4 September 2026. The width, height and video flag of each rendition sit beside it in **`mediaAssets`**, in the same order, rather than inside `images` where they would change the type of a column somebody else's code already reads. `mediaAssets` entries omit `width` and `height` on posts read from an account's grid: Instagram does not send dimensions there, and a `0` would be a measurement nobody made.

A profile row spells the two account flags the way the reference profile row does: **`private`** and **`verified`**, and the business category is **`businessCategoryName`**. They were `isPrivate` and `categoryName` until 4 September 2026.

### Input example

```json
{
  "directUrls": ["https://www.instagram.com/nasa/"],
  "resultsType": "posts",
  "resultsLimit": 30,
  "onlyPostsNewerThan": "3 days",
  "includeMediaUrls": false,
  "addParentData": true,
  "enrichPosts": true
}
```

`apify/instagram-scraper`'s own example input runs here unchanged — every key on its published form is on this one, and `tests/test_actor_variants.py` keeps a copy of that object so the day either side moves, the suite says so. To collect for a word instead:

```json
{
  "resultsType": "posts",
  "search": "specialty coffee",
  "searchType": "hashtag",
  "searchLimit": 48
}
```

### What this Actor does not collect

Public profiles, public posts, and the posts Instagram ranks for a keyword. A hashtag page, a place page, a story, a `/tagged/` URL and the reels *home* feed (`instagram.com/reels/` with no post after it) are **refused by name with an explanation** rather than quietly collected as something else — a green run over an empty dataset is the most expensive failure to diagnose, so it does not happen here. Those surfaces are not visible to a logged-out visitor at all, which is not a limit this Actor can lift.

**`search` reaches a ranked feed, not a hashtag page.** `searchType: hashtag` is the only value that answers, and what it answers with is Instagram's own popular-search feed: ask for `coffee` and you get what Instagram ranks for coffee, not every post carrying `#coffee`. `profile` and `user` would mean searching for *accounts* by name, which is behind the login curtain; `place` answers but its cursor returns the first page forever (measured 3 September 2026), so asking for two pages of a place would hand you the same posts twice. All three are refused with that measurement attached rather than answered with something else. Keyword posts arrive complete — each is read from its own page — because the feed node carries no timestamp, no like count and no comment count, and a column that is null for an invisible reason is worse than a slower run.

**What the reference scrapers put on a post row and this one does not.** Seven columns, down from twelve before the parity pass of 14-15 September 2026: `isPinned`, `isCommentsDisabled`, `videoDuration`, `videoPlayCount`, `musicInfo`, `coauthorProducers` and `paidPartnership`. Each is absent for the same reason — the logged-out node this Actor reads does not carry it — and a column nobody measured is left out rather than written as `null` or `0`. `musicInfo` and `coauthorProducers` need a reel with attached audio and a co-authored post to probe properly, and neither will be declared until one has been.

`alt`, `productType`, `locationName`, `locationId`, `childPosts`, `originalWidth`/`originalHeight`, `firstComment` and `latestComments` **left that list on 14 September 2026** and are on post rows now.

**On a profile row, `biography`, `profilePicUrl`, `externalUrl`, `externalUrls` and `highlightReelCount` are collected as of 14 September 2026**, under a data-controller decision recorded in this project's compliance file that set the ceiling at output parity with `apify/instagram-profile-scraper` and no further. `public_email` and `public_phone_number` arrive in the same payload and stay closed: the reference does not publish them either, and "it was in the response" has never been a purpose. `latestPosts` is still absent from this listing's rows because posts are rows of their own here — use **Instagram Profile Scraper** for the nested shape.

**The tagged accounts are collected minimised.** `taggedUsers` carries `username`, `id`, `isVerified`, `imageIndex` and `position` for each account tagged inside a picture — and deliberately **not** their display name or profile picture, which the other scrapers include. A tag says which account an image references, and the handle plus the id say that completely; a name and a face are personal data about somebody who is not even the profile you asked for. `mentions`, by contrast, is read out of the caption text, so the two columns answer different questions and neither is a substitute for the other.

**`enrichPosts` is what makes a listed post a complete post, and it is on by default.** Instagram's logged-out profile grid renders thumbnails, so its query carries a caption, a shortcode and one image and *nothing else*: no timestamp, no counts, no frame size and no tags. Each listed post is therefore read from its own page, which has all of it. That costs one request per post, against roughly one per twelve to list them — 100 posts is about 9 requests to list and about 109 to complete — so set `"enrichPosts": false` when you want shortcodes and captions quickly and can live with `timestamp`, `likesCount`, `commentsCount`, `videoViewCount`, `dimensions*` and `taggedUsers` being absent. When they are absent they are **absent, not empty**: `[]` and `0` would read as "this picture tags nobody" and "nobody liked it", which was never measured. A post collected by its own URL always carries them, whatever this flag says.

Single post URLs were on that list until 3 September 2026 and are now targets: paste `https://www.instagram.com/p/DCMUrLltVlM/` (or its `/reel/` and `/tv/` spellings) and you get exactly that post, with its comments if you ask for them. One unreachable profile or post in a list of fifty does not cost the other forty-nine.

### FAQ

**How fresh is the data?** Every profile and post row carries `collectedAt`. Answers come from a snapshot, not from a live call to Instagram at the moment you read them. A comment row does not carry it — it is the reference scraper's row and nothing else — so a comments run logs the snapshot's age instead.

**Why is `commentsCount` bigger than the comments I got?** `commentsCount` is the total Instagram declares; `commentsCollected` is how many bodies were actually walked, bounded by your limit.

**Why are comment runs slow?** The collector paces itself to look like a person rather than a script — roughly one comment per second per post. That pacing is why the data arrives at all.

**Is this legal?** It collects public data only. You are responsible for how you use it, including under GDPR where the data concerns people in the EU.

### Related

- **Instagram Comment Scraper with Sentiment** — the same engine, shaped for comment work, with a sentiment label and score on every comment, at the same $1.75 per 1,000.
- **Instagram Profile Scraper** — handles in, one row per account with its posts nested, at the same $1.75 per 1,000.

# Changelog

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

# Actor input Schema

## `directUrls` (type: `array`):

The profiles and posts to collect: profile URLs (https://www.instagram.com/nasa/), bare handles (nasa), or single post and reel URLs (https://www.instagram.com/p/DCMUrLltVlM/). The two kinds may be mixed. A post URL returns that post (or its comments); it cannot be used with resultsType 'details', which asks for an account's own figures. Hashtag, place, story and /tagged/ URLs are refused with an explanation rather than collected.

## `resultsType` (type: `string`):

'details' is the profile row alone. 'posts' and 'reels' return one row per post, reels being the video slice of the same feed. 'comments' returns one row per comment, each carrying the post it sits under.

## `resultsLimit` (type: `integer`):

Per profile. Posts and reels: up to 1000. Comments: up to 5000 PER POST, which is why the ceiling changes with the field above. Comments are the slow part - roughly one per second per post - so a large number here is a long run.

## `commentsPostLimit` (type: `integer`):

Only used when the result type is 'comments': how many of the profile's most recent posts to walk. The total comment count is this times the results limit, so both numbers set the length of the run together.

## `enrichPosts` (type: `boolean`):

Read each post's own page for the fields Instagram's profile grid does not carry: timestamp, likesCount, commentsCount, videoViewCount, dimensions and taggedUsers (the accounts tagged IN the picture, which no caption mentions). Leave it on for complete rows. Turning it off makes a listing about ten times cheaper in requests and leaves those fields null.

## `includeRelatedProfiles` (type: `boolean`):

Also collect the accounts Instagram suggests alongside each profile, published as relatedProfiles: handle, name, id, verified and private flags, avatar. It costs one extra request per profile, and the accounts in it are third parties you did not name — which is why it is off by default rather than always on.

## `onlyPostsNewerThan` (type: `string`):

A date (2026-07-01, or 2026-07-01T12:00:00Z) or a relative span ("3 days", "2 weeks"). Older posts are dropped. Leave empty for no cutoff.

## `search` (type: `string`):

A word to collect posts for, instead of or as well as the targets above. It reaches Instagram's popular-search feed, which RANKS posts for a word: this is not a hashtag page, so 'coffee' returns what Instagram ranks for coffee rather than every post carrying #coffee. The posts come back with their authors, and the author's own figures - followers, total posts - are not collected, because nobody named those accounts. Leave empty to collect only the targets above.

## `searchType` (type: `string`):

Which kind of thing the keyword names. Only 'hashtag' is reachable without a login and it reaches the keyword feed described above. 'profile' and 'user' would mean searching for accounts by name, which is behind the login curtain; 'place' would mean searching for places, whose feed answers but does not paginate - measured 2026-09-03, its cursor returns the first page forever. All three are refused with that measurement rather than silently returning something else.

## `searchLimit` (type: `integer`):

How many posts one keyword may return. Pages are 24, so a limit of 30 costs two pages. There is no 'all' setting on purpose: these are other people's posts, selected by a word rather than by anyone naming the account.

## `includeMediaUrls` (type: `boolean`):

Add the image and video URLs to each post row. They are signed by Instagram and expire within hours, so download anything you need promptly; the mediaExpired field says whether they were still valid when collected.

## `addParentData` (type: `boolean`):

Carry the owning account's username and id on every post and comment row. Turn it off for narrower rows when you are collecting one profile at a time.

## Actor input object example

```json
{
  "directUrls": [
    "https://www.instagram.com/nasa/",
    "https://www.instagram.com/natgeo/"
  ],
  "resultsType": "posts",
  "resultsLimit": 30,
  "commentsPostLimit": 12,
  "enrichPosts": true,
  "includeRelatedProfiles": false,
  "searchType": "hashtag",
  "searchLimit": 24,
  "includeMediaUrls": false,
  "addParentData": false
}
```

# Actor output Schema

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

One row per collected profile, post or comment.

# 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 = {
    "directUrls": [
        "https://www.instagram.com/nasa/",
        "https://www.instagram.com/natgeo/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("omaraw/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 = { "directUrls": [
        "https://www.instagram.com/nasa/",
        "https://www.instagram.com/natgeo/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("omaraw/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 '{
  "directUrls": [
    "https://www.instagram.com/nasa/",
    "https://www.instagram.com/natgeo/"
  ]
}' |
apify call omaraw/instagram-scraper --silent --output-dataset

```

## MCP server setup

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