# Instagram Scraper - Profiles, Posts, Hashtags & Comments (`lanternlab318/instagram-scraper`) Actor

- **URL**: https://apify.com/lanternlab318/instagram-scraper.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.99 / 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 - Profiles, Posts, Hashtags & Comments

Give it Instagram usernames, post or Reel URLs, or hashtags and get the public data back: profile details, and for every post its caption, hashtags, publish time, likes, comment count, views, image and video URLs and author - plus the post's comments, page after page, not just the first 15. No Instagram login, no cookies, no account of your own. **$1.99 per 1,000 results plus $0.005 per run.** See [Pricing](#pricing).

### What makes it different

- **Comments past the first page.** Instagram shows about 14 comments per page; this Actor keeps paging until it has the number you asked for. When Instagram stops early, the post says so (`commentsComplete: false` and `commentsPartialReason`) instead of passing a short list off as all there is.
- **Your limits are hard limits.** `maxPostsPerProfile`, `maxPostsPerHashtag` and the comment limits are never exceeded, and you are never charged for more than you asked for.
- **The date filter works.** `newerThan` keeps only posts published since that date. On a profile it stops at the first older post *below the pinned ones*, so an old pinned post never cuts the list short.
- **Pinned posts once.** Pinned posts carry `isPinned: true`, are written at most once per run, and can be left out with `skipPinnedPosts`.
- **Hashtag results really carry the hashtag.** A post is returned for `#moon` only if its own caption contains `#moon`. Posts that merely mention the word are read, dropped and not charged.
- **No duplicates.** Profiles, posts and comments are deduplicated across the whole run by Instagram's own IDs; a repeat is written and charged once. A post URL whose post an earlier profile or hashtag input already wrote still gets the comments it asks for.
- **Empty, blocked and failed is free - and labelled.** Every input gets a free `input_status` row. An input Instagram refused, or one cut short by a limit, is marked `partial: true` with a reason - never reported as "zero posts".

### Input

```json
{
    "usernames": ["nasa"],
    "postUrls": ["https://www.instagram.com/p/DX7RFLbk8B6/"],
    "hashtags": ["artemis"],
    "maxPostsPerProfile": 24,
    "maxPostsPerHashtag": 20,
    "maxCommentsPerPost": 100,
    "newerThan": "30 days",
    "skipPinnedPosts": true
}
```

| Field | What it does |
| --- | --- |
| `usernames` | `nasa`, `@nasa` or a profile URL. Returns a profile row plus its newest posts. |
| `postUrls` | `/p/<code>/` or `/reel/<code>/` URLs. Returns the post plus its comments. |
| `hashtags` | `tag` or `#tag`. Returns public posts whose caption carries the hashtag. |
| `maxPostsPerProfile` | Posts per profile, newest first. Default 12, up to 500. |
| `maxPostsPerHashtag` | Posts per hashtag. Default 20, up to 100. |
| `maxCommentsPerPost` | Comments per post URL. Default 50, up to 5,000; 0 turns them off. |
| `maxCommentsPerListedPost` | Comments per profile or hashtag post. Default 0 (off). |
| `newerThan` | `2026-10-01`, an ISO time, or `7 days` / `2 weeks` / `3 months`. Applies to profile and hashtag posts. |
| `skipPinnedPosts` | Leave pinned posts out of profile results. Default off. |
| `maxUpstreamRequests` | Optional hard ceiling on requests. By default the Actor works one out from your inputs, so a run is never unbounded. |
| `proxyConfiguration` | Default: Apify Proxy (datacenter). A refused request is retried once on a residential IP. |

### Output

Every row has a `recordType`. Dataset views: Posts, Comments, Profiles, Input status.

**post**

```json
{
    "recordType": "post",
    "postId": "3889777825826455674",
    "shortcode": "DX7RFLbk8B6",
    "url": "https://www.instagram.com/p/DX7RFLbk8B6/",
    "type": "Sidecar",
    "productType": "carousel_container",
    "caption": "Start your week with some new photos from Artemis II! ...",
    "hashtags": ["nasa", "artemis", "photodump"],
    "mentions": [],
    "publishedAt": "2026-05-04T18:01:15.000Z",
    "publishedAtEpoch": 1777917675,
    "likeCount": 397148,
    "commentCount": 1326,
    "viewCount": null,
    "displayUrl": "https://scontent-....cdninstagram.com/...",
    "videoUrl": null,
    "mediaUrls": ["https://scontent-...", "https://scontent-...", "https://scontent-...", "https://scontent-..."],
    "ownerId": "528817151",
    "ownerUsername": "nasa",
    "ownerFullName": "NASA",
    "ownerIsVerified": true,
    "coauthors": ["nasaartemis"],
    "locationName": null,
    "isPinned": null,
    "commentsRequested": 100,
    "commentsCollected": 100,
    "commentsComplete": true,
    "commentsPartialReason": null,
    "inputKind": "post",
    "inputValue": "https://www.instagram.com/p/DX7RFLbk8B6/",
    "rank": 1,
    "partial": false,
    "partialReason": null,
    "collectedAt": "2026-10-30T12:54:08.945Z"
}
```

`type` is `Image`, `Video` or `Sidecar` (a carousel); `productType: "clips"` is a Reel. `publishedAt` is Instagram's publish time; `collectedAt` is when this run read it. `isPinned` is `true`/`false` on profile posts and `null` where Instagram does not say. `viewCount` is filled where Instagram shows it to logged-out visitors (profile Reels and videos) and is `null` elsewhere - it is never estimated.

**comment** - `commentId`, `postId` (the parent post), `postShortcode`, `postUrl`, `parentCommentId` (the comment it replies to; `null` on a top-level comment), `text`, `publishedAt`, `likeCount`, `ownerUsername` (the commenter's public username only), `ownerIsVerified`, `rank`, `collectedAt`.

**profile** - `userId`, `username`, `fullName`, `biography`, `externalUrl`, `followersCount`, `followsCount`, `postsCount`, `isVerified`, `isPrivate`, `isBusinessAccount`, `category`, `profilePicUrl`, `profileUrl`, `collectedAt`. No e-mail addresses or phone numbers are collected.

**input_status** (free) - one per input: `postsReturned`, `commentsReturned`, `rowsCharged`, `duplicatesSkipped`, `pinnedSkipped`, `olderSkipped`, `unrelatedSkipped`, `partial`, `partialReason` and a plain-English `statusText`.

**RUN_SUMMARY** (free) - the last row, and the same object in the key-value store: totals, partial inputs by reason, requests sent, and why the run stopped if it stopped early.

#### Partial reasons

| `partialReason` | Meaning |
| --- | --- |
| `comments_not_public` | Instagram stopped paging comments before its own count. The count includes replies and comments it does not show to logged-out visitors. |
| `search_results_exhausted` | A hashtag's public search results ran out before `maxPostsPerHashtag`. |
| `candidate_limit` | A hashtag read 3 search results per post asked for without filling the limit (most did not carry the hashtag or were filtered out). |
| `private_account` | The profile is private; its profile row is returned, its posts are not public. |
| `not_found` | No public profile or post by that name (deleted, renamed, restricted or never existed). |
| `gated` | Instagram withholds the post from logged-out visitors (age or region restriction). |
| `blocked` | Instagram refused the requests, also on a fresh residential IP. Not an empty result. |
| `upstream_error` / `unexpected_shape` | A request failed, or the answer was not in a shape the Actor knows. Not an empty result. |
| `upstream_request_cap` | The run reached `maxUpstreamRequests`. |
| `charge_limit_reached` | The maximum charge you set for the run was reached. |
| `not_collected` | The run stopped before reaching this input. |

### Limits, stated up front

- **Public content only, no login.** Private accounts, Stories, highlights, and anything that needs an Instagram account are out of scope.
- **Hashtags come from public web search.** Instagram's own hashtag feed requires a login. This Actor finds hashtag posts through public search results, then checks each post's caption. You get real, matching posts - but not Instagram's complete or newest-first hashtag feed, and popular accounts are over-represented. When the search results run out the hashtag is marked `search_results_exhausted`.
- **Replies are not shown to logged-out visitors.** Comments are top-level comments; `parentCommentId` is filled only when Instagram provides it. Instagram's `commentCount` includes replies, which is why a post can have fewer collectable comments than its count.
- **Views.** Instagram shows a view count logged-out only for some videos; elsewhere `viewCount` is `null`.
- **Transcripts** of Reels are not part of this Actor.

### Pricing

Pay per result: **$1.99 per 1,000 results**, plus **$0.005 per run** (start fee). A result is one `profile`, `post` or `comment` row. `input_status` rows, the run summary, duplicates, filtered posts and anything an input could not return are free, so a blocked or empty input costs nothing beyond the start fee. Example: one profile with 50 posts and 20 comments each = 1 + 50 + 1,000 = 1,051 results ≈ $2.10.

### How it works

Plain HTTP requests to the same public endpoints Instagram's own logged-out web page uses, through Apify Proxy (datacenter by default, residential only as a fallback). Profiles come from Instagram's public profile endpoint (newest 12 posts in one request, then the profile grid's own pages); posts and comments from the logged-out post page and its comment pages. No browser, no login, no cookies of your own.

# Actor input Schema

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

One profile per line. "nasa", "@nasa" and "https://www.instagram.com/nasa/" are all accepted. Each one returns a profile row (name, bio, follower, following and post counts, verified badge) plus its newest posts, up to Max posts per profile. Private accounts return the profile row only and are marked partial with partialReason "private_account".

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

One post per line: https://www.instagram.com/p/<code>/ or https://www.instagram.com/reel/<code>/ (a username in front and query strings are fine). Each one returns that post and up to Max comments per post URL of its comments, read page after page.

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

One hashtag per line, with or without "#". Each one returns public posts whose own caption carries that hashtag; search results that merely mention the word are read and dropped, and never charged. Instagram's own hashtag feed requires a login, so posts are found through public web search results: they are the matching posts the public web lists, not Instagram's complete or newest-first feed.

## `maxPostsPerProfile` (type: `integer`):

Upper bound on the post rows one profile may produce, newest first. Never exceeded. Up to 12 come in one request; more are read page by page.

## `maxPostsPerHashtag` (type: `integer`):

Upper bound on the post rows one hashtag may produce. Never exceeded. When the public search results run out first, the hashtag is marked partial with partialReason "search_results_exhausted" instead of being reported as complete.

## `maxCommentsPerPost` (type: `integer`):

Comments to collect for each post in Post or Reel URLs. Instagram shows about 14 per page; the Actor pages on until this number is reached or Instagram has no more. 0 turns comments off. When Instagram stops before its own comment count, the post is marked with commentsPartialReason "comments_not_public": its count includes replies and comments it does not show to logged-out visitors.

## `maxCommentsPerListedPost` (type: `integer`):

Comments to collect for each post found through usernames or hashtags. Off (0) by default, because every post then needs its own requests.

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

Leave empty for no date filter. A date (2026-10-01), a full time (2026-10-01T08:00:00Z) or a relative period ("7 days", "2 weeks", "3 months"). Applies to profile and hashtag posts. A profile stops reading at its first post older than this, below its pinned posts, so an old pinned post never ends the list early. Post URLs you name yourself are always collected.

## `skipPinnedPosts` (type: `boolean`):

Leave pinned posts out of profile results. Either way a pinned post is written at most once per run and carries isPinned: true, so it is never counted twice.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends. At the ceiling collection stops: what was read is kept, unfinished inputs are written with partialReason "upstream_request_cap" and the run ends normally. Leave it empty and the Actor works a ceiling out from your own inputs and limits, so a run is never unbounded.

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

The proxy requests go through. The default (Apify Proxy, datacenter) is what this Actor is measured on. A request Instagram refuses is retried once on an Apify residential IP before the input is marked partial.

## Actor input object example

```json
{
  "usernames": [
    "nasa"
  ],
  "maxPostsPerProfile": 12,
  "maxPostsPerHashtag": 20,
  "maxCommentsPerPost": 50,
  "maxCommentsPerListedPost": 0,
  "skipPinnedPosts": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (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 = {
    "usernames": [
        "nasa"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/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"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/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"
  ]
}' |
apify call lanternlab318/instagram-scraper --silent --output-dataset

```

## MCP server setup

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