# Instagram Comments Scraper - Date Filter & New-Comment Monitor (`scrapersdelight/instagram-comments-scraper`) Actor

From $0.30 per 1,000. Scrape top-level comments on any public Instagram post or reel: text, GIF, likes, timestamp, commenter username, ID and verified badge. No login or cookies. Comments come back newest-first, so a date filter or the built-in monitor pulls only new ones. Replies not included.

- **URL**: https://apify.com/scrapersdelight/instagram-comments-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Social media, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 per comment returneds

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?

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 Comments Scraper — date filter & new-comment monitor

**Scrape the top-level comments on any public Instagram post or reel. No login, no cookies, no
browser. From $0.30 per 1,000 comments — roughly an eighth of what the category leader lists.**

Paste a post URL. Get every comment's text (or GIF), likes, exact timestamp, deep link, and the
commenter's username, numeric account id, verified badge and profile URL — plus the hashtags,
@mentions, links and any email address the commenter typed, pulled out of the text for you.

> ### ⚠️ Read this first: **threaded replies are NOT included.**
>
> This Actor reads Instagram's login-free comment stream, and that stream carries **top-level
> comments only**. `child_comment_count` was `null` on **3,189 of 3,189** comments measured, and
> `parent_comment_id` on **0 of 3,189**. Every row you get says so in its `repliesIncluded` column.
>
> Measured against Instagram's own comment count (which *does* include replies), a fully-read
> thread here comes to **64%–83%** of the total — the rest are replies. Four threads read to the
> very end on 2026-09-07:
>
> | Post | Top-level comments returned | Instagram's own count | Coverage |
> |---|---|---|---|
> | `DZ5T2XPllXv` | 397 | 616 | **64.5%** |
> | `Dc1U6TYGwJE` | 415 | 561 | **74.0%** |
> | `Dcv8Q_Pk06P` | 444 | 596 | **74.5%** |
> | `DczLHKOGpqh` | 774 | 934 | **82.9%** |
>
> If you need replies, several other Actors in this category ship them and you should use one of
> those. We would rather lose the sale than put a capability in the title we could not prove.

***

### 🎯 What this one does that the other 91 don't

Instagram serves this comment stream in **strict reverse-chronological order**. That is not a
claim, it is a measurement: **0 inversions of `created_at` across 3,189 comments on 18 posts**, and
0 again across a 2,030-row full-thread run. Two things fall out of that fact, and neither is
comfortable for a scraper reading Instagram's *ranked* comment endpoint:

#### 1. A date filter that actually stops early — so you pay for the window, not the thread

Set **Only comments newer than** to `7 days`, `48 hours` or `2026-09-01`, and the crawl **stops the
moment the stream crosses the boundary**. It does not download the thread and filter afterwards.

Measured on the same 2,118-comment post, same day:

| Run | Calls | Rows delivered | Wall time |
|---|---|---|---|
| Whole top-level thread | **148** | 2,118 | 106 s |
| `commentsNewerThan: 2026-06-25` | **2** | 1 | 3.3 s |

Same post. 74× fewer calls, and you are billed for the rows in your window instead of all 2,118.
On a 79,000-comment post asking for "yesterday", the difference is three orders of magnitude.

#### 2. A monitor that is exact, not heuristic

Switch on **Monitor mode**, put the Actor on a schedule, and each run delivers **only the comments
posted since the previous run**. The last-seen comment id lives in a named key-value store and
paging stops exactly at it — no dedupe guessing, no re-billing comments you already have.

And when a burst is bigger than your per-run cap, it does **not** silently drop the overflow. The
watch pointer is held back and the comments already shipped are remembered, so the next run pages
straight through them and picks up the backlog underneath. Verified live: run 1 delivered 8
comments, run 2 skipped exactly those 8, read 2 pages and delivered 8 **different** ones —
**0 overlap, 0 double billing, 0 lost comments.**

#### 3. An honest coverage ledger on every row

`topLevelComplete`, `dateWindowComplete`, `truncationReason`, `postCommentsScanned` and
`topLevelCoveragePct` tell you exactly what fraction of the thread you got and what stopped the
read. Nothing else in this category tells you that.

And "complete" here needs **three witnesses, on two different IPs**. Instagram's logged-out comment
stream intermittently answers `has_next_page: false` in the middle of a thread — measured **1 short
answer in 42 calls** — and an Actor that believes it tells you it read a 2,118-comment thread to the
end when it read 15.

Re-issuing the call is not enough: on 2026-09-09 that short answer came back **twice in a row on the
same session** for a post this Actor had read 2,119 comments from ninety minutes earlier. The short
answer belongs to the exit IP, so the repeat is not an independent witness. An end-of-stream answer
is now confirmed once on the same session and then **again from a fresh proxy IP on a newly minted
session**, and if that third opinion cannot be obtained the read is recorded as
`truncationReason: "end-not-independently-confirmed"` rather than as complete. Every confirmation
page's rows are discarded, so none of this can double-bill you. A supposedly complete read that
lands under 20% of Instagram's own comment count is still downgraded to
`truncationReason: "instagram-short-stream"`. `POST_STATUS` carries `endOfStreamConfirmed` and
`instagramFalseEndsRetried` so you can see it happen.

#### 4. A **work** budget, not just a delivery cap

`maxCommentsPerPost` bounds what is *delivered*. A filter or a date window drops a comment **after**
it has been read, so "the 10 comments mentioning *discount*" can quietly mean reading a
79,000-comment thread to find them — a long, expensive run behind a five-row invoice.

`maxCommentsScannedPerPost` bounds the **reading**. Left at `0` it is automatic: **no budget at all**
when nothing in your input can drop a row (then every comment read is a comment delivered), and
otherwise **100 comments read per row you asked to be delivered**, floor 1,000, ceiling 20,000 — so
the default `maxCommentsPerPost: 50` plus any filter gives a 5,000-comment search per post. Set your
own number to search deeper; `500000` is effectively no budget.

Measured A/B, same input, both builds started at the same moment (2026-09-09) — a `textIncludes`
term matching nothing on the 2,118-comment thread, **at `maxCommentsPerPost: 10`**, so the automatic
budget is its 1,000 floor:

| | Without the work budget | With it |
|---|---|---|
| Comments read | 2,119 | **1,009** |
| Instagram calls | 148 | **72** |
| Wall time on the post | 112.6 s | **37.9 s** |
| Rows delivered / **billed** | 0 / **$0** | 0 / **$0** |
| Reported as | `topLevelComplete: true` | `truncationReason: "scan-budget"` |

Same answer, 2.7× less work — and, importantly, the run now **tells you** it stopped early instead of
implying the thread had nothing.

**The budget is proportional, so read that table with its cap in mind.** It scales with the rows you
asked for, which means it bites hardest on the runs that would waste the most: the 79,000-comment
thread above is stopped 16× early at the default cap, while a thread smaller than your budget is
still read in full. Re-measured on that same filter-matches-nothing input at the **default**
`maxCommentsPerPost: 50` — a 5,000-comment budget against a 2,118-comment thread — the budget never
trips and the whole thread is read: 2,119 comments, 0 rows, **$0 charged**, `scanBudgetReached:
false` (run `DFYxmxcpJdiOKsghL`, build 0.2.12). Lower `maxCommentsPerPost`, or set
`maxCommentsScannedPerPost` yourself, if you want a search that stops sooner than that.

When it stops a post, the run **says so and names the filter**, in the log, in the status message and
as `truncationReason: "scan-budget"` — because a short answer that reads like "that's all Instagram
had" is a lie when it was our bound that stopped the read. **Comments read are never billed**; only
comments delivered are. `POST_STATUS` carries `commentsScanBudget` and `scanBudgetReached`.

#### 5. GIF comments are handled, not shipped as blank rows

GIF comments come back from Instagram with `text: ""`. Measured **85 of 1,200 rows (7.1%)**, and
**every single one** carried a live GIF URL. They are typed `commentType: "gif"` with `gifUrl`,
`gifProxiedUrl` and `gifId` filled in, instead of landing in your CSV as empty comments.

#### 6. Resumable pagination for very big threads

Switch on **Resumable pagination** and each post's cursor is stored, so the next run continues the
backfill where the last one stopped instead of paying to re-read the head. Verified live: two
consecutive 20-row runs on the same post, **0 overlapping rows**.

#### 7. It repairs itself when Instagram changes

This route rides a Meta persisted-query id, and Meta rotates those on deploys. Most scrapers built
this way simply return zero rows one morning and exit *successfully*. This one detects the rejected
id, re-mints its session, retries with the ids compiled into the build, and only then sweeps
Instagram's own JavaScript bundles for the new id, caches it and carries on.

Verified by deliberately poisoning both stored ids on the live platform (2026-09-08): the Actor
detected the explicit GraphQL rejection, re-minted, swept 253 of 455 bundles, recovered both ids in
**48 s** and delivered its rows. Both ids are then re-validated — page 1 *and* a paginated page 2 —
before they are cached, so a half-recovered pair can never be stored. Four such runs at once put
one sweep past its own budget, so the cheap repair now runs first: if the stored ids are stale but
the ids built into this Actor still work, the route comes back in **two calls instead of a
48-second sweep**. If it cannot repair the route at all, the run **FAILS loudly** and says the
problem is ours — it never reports a broken route as "this post has no comments".

***

### 📊 What every row contains

**The comment**

- 💬 `text` — the comment body · *non-empty on 1,113 of 1,200 measured rows (92.8%)*
- 🎬 `commentType` (`text` / `gif` / `empty`), `gifUrl`, `gifProxiedUrl`, `gifId`
- ❤️ `likeCount` · *100% present*
- 🕐 `createdAt` (ISO 8601) + `createdAtUnix` · *100% present*
- 🔗 `commentUrl` — deep link straight to the comment
- 🆔 `commentId` — Instagram's own stable comment id · *100% present; 0 duplicates within every measured run (3,189 / 2,030 / 1,200 rows)*
- 🚩 `isCovered`, `restrictedStatus`, `hasTranslation`
- 🧵 `parentCommentId`, `replyCount`, `repliesIncluded` — always null / null / false on this route,
  kept as columns so the limitation is visible **in the data**, not only in the docs

**The commenter**

- 👤 `ownerUsername` · *100%* · and `ownerProfileUrl`
- 🔢 `ownerId` (numeric account id) + `ownerGraphId` · *100%*
- ✅ `ownerIsVerified` · *true on 78 of 2,030 rows (3.8%) in a full-thread run*
- 🖼️ `ownerProfilePicUrl` — signed CDN link, **it expires**; fetch it now, don't archive the URL
- 👑 `isPostOwner` — matched on numeric id, so it works even without the post-context fetch

**Pulled out of the comment text** (read from what the commenter typed — not a lookup, not enrichment)

- `#️⃣ hashtags` · *0.7% of rows* · `@ mentions` · *3.4%* · `🔗 urlsInText` · *0.6%* ·
  `📧 emailsInText` · *0.1%* · `wordCount`, `textLength`

**The post**

- `postShortcode`, `postUrl`, `postMediaId`, `postMediaType` (`image` / `carousel` / `video` — all
  three verified live), `postOwnerId`, `postOwnerGraphId`, `postTopics`
- With **Add post context** on: `postOwnerUsername`, `postCommentCountTotal` (Instagram's own, incl.
  replies), `postLikeCount`, `postCaption`, `postTakenAt`, `postProductType`,
  `topLevelCoveragePct` — *100% filled across 8 of 8 posts measured*.
  These are read from **your post's own media object**, located by its shortcode in the page. That
  matters: the post page also ships the account's other recent posts, each with its own caption and
  `product_type`, so a scraper that takes the first match on the page hands you a **different
  post's** caption. (We shipped that bug for one build and caught it on 2026-09-08: two different
  posts came back with the same 701-character caption. Every context field is now anchored, and the
  offline test suite asserts the anchoring against captured live bytes.)

**The ledger**

- `topLevelComplete`, `dateWindowComplete`, `truncationReason`, `postCommentsScanned`,
  `postCommentsReturned`, `commentIndex`, `isNewSinceLastRun`, `scrapedAt`

A per-post ledger is also written to the run's **`POST_STATUS`** key-value record: pages read,
comments scanned, the scan budget in force (`commentsScanBudget`) and whether it stopped that post
(`scanBudgetReached`), rows returned, rows filtered out, duplicates suppressed, whether the thread
was read to the end, what stopped it, and coverage against Instagram's own count.

***

### ⚡ Measured performance

All numbers below are from real runs, not estimates. The first block is **on the Apify platform at
256 MB** — what you actually get. The second is the recon measurement set through an Apify
datacenter proxy on 2026-09-07.

**On-platform runs** (build 0.2.x, 2026-09-08 — 45 runs, including a full adversarial re-test)

| Run | Rows | Charged | Duration |
|---|---|---|---|
| 3 posts x 25 comments, post context on | 75 | 75 comments + 3 contexts = **$0.0285** | **6.6 s – 71 s** (median ~8 s) |
| Empty input `{}` (falls back to the documented sample) | 75 | $0.0285 | 7.0 s – 57 s |
| One 397-comment thread read to the very end, context on | 397 | **$0.1211** | 18 s – 126 s, depending on the proxy exit |
| Date window, 21 of 29 comments outside it | 8 | $0.0024 — the 21 were **not** billed | 3.7 s |
| Charge cap set to $0.01 | **26** | $0.0098, cap named in the status message | 15.0 s |
| Monitor seed / Monitor with nothing new | 4 / 0 | $0.0062 / $0.0050 | 3.4 s / 3.9 s |
| Stored query ids poisoned -> self-heal | 5 | $0.0015 | 5.7 s (built-in retry) / 77 s (full bundle sweep) |

In **every** one of those runs, `chargedEventCounts` equalled the number of rows in the dataset, and
neither Apify auto-event (`apify-actor-start`, `apify-default-dataset-item`) appeared on the bill.

The wall-time spread is **entirely** the proxy exit you draw, not the parser: every one of those
runs recorded 100% HTTP 200, and the slow ones simply averaged 3.0 s per call instead of 0.4 s. The
Actor bounds every request by the run's own clock, so a bad exit costs you time, never a timeout
with an empty dataset.

**Throughput and reliability (recon set, Apify datacenter proxy)**

| Run | Posts | Rows | Calls | HTTP 200 | Avg call |
|---|---|---|---|---|---|
| 8 posts x 150 comments, context on, concurrency 4 | 8 | **1,200** | 88 | **100%** | 689 ms |
| 4 posts read to the very end, concurrency 4 | 4 | **2,030** | 142 | **100%** | 1,195 ms |
| Deep single-post pagination | 1 | 2,118 | 148 | **100%** | 710 ms |
| Sustained load, one minted session | 18 | 3,189 | 228 | **100%** | 932 ms |

- **0 duplicate comment ids** across every one of those runs.
- **0 ordering inversions** — the property the date filter and the monitor are built on.
- **~1.08 KB per comment** on the wire.
- **~13-15 comments per call**, whatever page size is requested (`first` = 12, 24, 50 and 100 all
  returned 14 edges on the same post — this Actor never assumes otherwise).
- One minted session held **176 consecutive calls at 100%**; the Actor mints once per run and only
  re-mints (on a fresh proxy IP) if a page starts failing.
- **Proxy:** datacenter `auto` is the default — 228/228 at ~932 ms. Residential US also works
  (10/10, ~1,049 ms) but is slower and costs bytes for no gain. One transient proxy `ECONNRESET`
  in ~350 calls; it is retried on a new IP, not treated as a block.
- **Memory:** 256 MB. HTTP only — no browser anywhere in this Actor.

**Rough guide:** a 400-comment thread ≈ 28 calls. Wall time depends on the proxy exit you draw — the same 28-call thread measured **21 s** and **126 s** on the same day (avg call 0.7 s vs 3.6 s, 100% HTTP 200 on both). A 2,000-comment thread ≈ 148 calls
≈ 106 s. A 79,000-comment thread is ~5,400 calls — use the date filter, the per-post cap, or
Resumable pagination rather than doing that in one run.

***

### 💵 Pricing

Plain pay-per-event. **No Actor-start charge, no per-dataset-item charge** — you pay for comments,
nothing else.

| Event | Price | When it fires |
|---|---|---|
| **Per comment returned** | **$0.0003** (**$0.30 / 1,000**) | Each top-level comment delivered to your dataset |
| Per post context | $0.002 | Once per post whose page is fetched for the owner handle, Instagram's own comment count and the coverage ledger. Only when the toggle is on, and never for a post that returned no comments |
| Per monitoring run | $0.005 | Once per Monitor-mode run. Never in normal Scrape mode, and never when Instagram returned nothing usable |
| Per alert delivered | $0.002 | Each new-comment alert whose delivery is **proven**: a webhook or Slack post that returned 2xx, or an email digest whose `apify/send-mail` run came back `SUCCEEDED`. Once per sink per comment; once per recipient for the email digest |

**What you are not charged for**, by construction:

- comments a **filter** removed — filters run before delivery
- comments **outside your date window** — the date bound runs before delivery
- comments the Actor **read but did not deliver** — searching is our cost, not yours; the
  `maxCommentsScannedPerPost` work budget bounds it and names itself when it stops a post
- an alert whose delivery **failed, was refused, or did not finish** — webhooks need a 2xx and the
  email digest needs a `SUCCEEDED` send-mail run before a cent is billed
- **duplicate** comment ids — de-duplication runs before billing
- comments on a post Instagram reported as **unavailable**
- a post-context fetch on a post that **delivered no rows**
- a monitoring run where **Instagram never answered** — nothing was checked, so nothing is billed
- anything at all past your `maxTotalChargeUsd` — the run reserves the money for each row *before*
  delivering it, and names the cap as the reason if a batch is trimmed

**Worked examples**

| Job | Cost |
|---|---|
| 1,000 comments, no post context | **$0.30** |
| Yesterday's comments on a big launch post (~50 rows) + context | **$0.017** |
| A whole 2,118-comment thread + context | **$0.64** |
| A daily monitor on 5 posts finding ~40 new comments a day | **$0.017 / day** |

#### How that compares, read off the live Store on 2026-09-08

| Actor | Listed per comment | 1,000 comments | Monthly users |
|---|---|---|---|
| **This Actor** | **$0.0003** | **$0.30** | new |
| `apify/instagram-comment-scraper` (category leader, 4.52★) | $0.0019 – $0.0023 | $1.90 – $2.30 | 4,664 |
| `apidojo/instagram-comments-scraper-api` | $0.0005 / item **+ $0.0075 per post queried** | $0.50 + post fees | 409 |
| `scrapesmith/instagram-comments-scraper` | $0.0005 / item + $0.00005 start | $0.50 | 338 |
| `datadoping/instagram-comments-and-replies-scraper` | $0.00155 / item | $1.55 | 261 |

That is **6× to 8× under the category leader's listed rate** and 40% under the cheapest rate among
the actors in this category that have real monthly traffic.

**Not the cheapest number on the Store, and we are not going to pretend otherwise.** A handful of
near-dormant listings price a dataset item at $0.00001 — `omissive_zen/instagram-comment-scraper`
(6 monthly users), `scrapers-hub/instagram-comments-scraper` (1), `dead00/instagram-comment-bot`
(23). If a headline number is all you are shopping for, they are cheaper than this Actor. What we
claim is the cheapest *maintained* per-comment rate in the working half of this category, with the
date filter, the monitor and the coverage ledger on top.

***

### 🚀 Getting started

**The simplest run:**

```json
{
  "startUrls": [{ "url": "https://www.instagram.com/p/DZ5T2XPllXv/" }],
  "maxCommentsPerPost": 50
}
```

**Only comments from the last 48 hours, on three posts:**

```json
{
  "startUrls": [
    { "url": "https://www.instagram.com/p/DZ5T2XPllXv/" },
    { "url": "https://www.instagram.com/reel/Dcv8Q_Pk06P/" },
    { "url": "https://www.instagram.com/p/DczLHKOGpqh/" }
  ],
  "commentsNewerThan": "48 hours",
  "maxCommentsPerPost": 0
}
```

**Watch a launch post and get Slack alerts** (put this on an hourly Apify schedule):

```json
{
  "startUrls": [{ "url": "https://www.instagram.com/p/DZ5T2XPllXv/" }],
  "monitorMode": true,
  "monitorSeedComments": 0,
  "includePostContext": false,
  "slackWebhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ"
}
```

**Mine a giveaway thread for entrants who left a contact:**

```json
{
  "startUrls": [{ "url": "https://www.instagram.com/p/DZ5T2XPllXv/" }],
  "maxCommentsPerPost": 0,
  "commentsNewerThan": "2026-09-01",
  "commentsOlderThan": "2026-09-08",
  "textIncludes": ["done", "entered", "count me in"],
  "requireEmailInText": true
}
```

**Back-fill a 79,000-comment thread over several cheap runs:**

```json
{
  "startUrls": [{ "url": "https://www.instagram.com/p/DZ5T2XPllXv/" }],
  "maxCommentsPerPost": 5000,
  "resumeMode": true
}
```

Run it again with the same input and it continues where it stopped.

***

### 🧰 Who buys this

- **Social listening / brand monitoring** — watch a campaign post and route new comments into Slack
  the minute they land.
- **Crisis and PR watch** — a post going sideways generates comments faster than a human can read
  them; the monitor gives you a live feed, filtered to verified accounts if you want only the ones
  that matter.
- **Giveaway and competition compliance** — pull exactly the comments posted inside the entry
  window, with `commentsNewerThan` + `commentsOlderThan`, and prove it with the coverage ledger.
- **Comment-to-lead workflows** — filter to comments carrying an email or a link, or to comments
  matching your buying-intent keywords ("price", "shipping", "where can I buy").
- **Creator and influencer research** — engagement-quality analysis on the commenter side:
  verified share, like distribution, GIF vs text, mention graphs.
- **Data teams** — one row per comment, one stable key set, CSV / JSON / Excel export, or call it
  from the API and schedule it.

***

### 🔧 Every input, and what it actually does

Nothing in this list is decorative — each one is wired to real behaviour.

**What to scrape** — `startUrls` (post / reel / IGTV URLs), `shortcodes` (bare codes),
`maxCommentsPerPost` (how many rows you get), `maxCommentsScannedPerPost` (how many comments the
Actor will *read* looking for them — see §4 above), `maxComments`.

**Date window** — `commentsNewerThan`, `commentsOlderThan` (ISO date, timestamp, or a span like
"7 days"), `dateBoundGracePages`.

**Monitor** — `monitorMode`, `monitorStateKey` (run several independent watches on one post),
`monitorSeedComments`, `alertOnNewComment`, `webhookUrl`, `slackWebhookUrl`, `emailRecipients`.

> **About the email digest.** It is sent by calling `apify/send-mail`, so it needs an Apify plan that
> permits **running public Actors** (standard plans do; some developer-only plans do not — ours is one,
> which is how the failure path below was verified live). `apify/send-mail` itself accepts only your
> own account address on the free plan, and up to 20 recipients on a paid one. If the send is
> refused, fails, or does not finish, the run **says which and bills nothing** — `alert-delivered`
> is charged only against a `SUCCEEDED` send-mail run. Webhook and Slack alerts have no such
> dependency; they need only a 2xx from your endpoint.

**Filters** (all run before delivery, so nothing filtered out is billed) — `commentTypes`,
`minLikeCount`, `onlyVerifiedCommenters`, `textIncludes`, `textExcludes`, `requireEmailInText`,
`requireLinkInText`, `excludePostOwnerComments`, `onlyCommenters`, `excludeCommenters`.

**Output** — `includePostContext`, `includeRawComment`, `deduplicateComments`.

**Resume & recovery** — `resumeMode`, `resetResumeState`, `docIdRecovery`.

**Performance & proxy** — `maxPagesPerPost`, `maxSecondsPerPost`, `maxConcurrency`,
`requestTimeoutSecs`, `maxRequestRetries`, `maxBufferedCommentsPerPost`, `proxyConfiguration`.

***

### ❓ FAQ

**Does it get threaded replies?**
No. The login-free route this Actor rides carries top-level comments only — `child_comment_count`
was null on 3,189 of 3,189 comments measured. Every row says `repliesIncluded: false`, and the
coverage ledger tells you what share of the thread that leaves you with (64%–83% on the four
threads measured). Other Actors in this category do ship replies; use one of those if you need them.

**Do I need an Instagram login, cookies or a session id?**
No. Nothing to configure, nothing to keep alive, no account to put at risk.

**Does it work on reels?**
Yes. Image posts, carousels and video/reel posts were all verified live and are reported as
`postMediaType` `image` / `carousel` / `video`.

**Can I give it a profile URL and get every post's comments?**
No, and that is deliberate. One site, one job: this Actor reads the comments of the posts you give
it. Pair it with a post scraper if you need to discover the URLs first.

**Are the comments in order?**
Newest first, strictly — 0 inversions of the timestamp across 3,189 comments on 18 posts and 2,030
more on a full-thread run. `commentIndex` gives you the position in that stream.

**Can I get the top / most-liked comments first?**
Not on this route — Instagram serves the logged-out stream chronologically, and this Actor will not
pretend otherwise by re-sorting a partial read and calling it "top comments". Read the thread (or a
date window) and sort by `likeCount` yourself; that is the only honest way to do it here.

**How do I get only new comments each day?**
Monitor mode plus an Apify schedule. Set `monitorSeedComments: 0` if you want the first run to just
record a baseline and deliver nothing.

**What happens if more new comments arrive than my per-run cap?**
Nothing is lost and nothing is billed twice. The watch pointer is held back and the comments
already delivered are remembered, so the next run pages through them without re-delivering and
picks up the backlog underneath. The run tells you it happened.

**What if the post is deleted or private?**
Instagram answers with an empty media object. The run reports that post as **unavailable**, does
not bill you for it, and — if *every* post you gave came back that way — checks the Actor's own
known-live sample in the same run so it can tell you whether the problem is your posts or our
scraper. It will never blame Instagram for something we did.

**Will a zero-row run still charge me?**
No. In Scrape mode nothing is billed. In Monitor mode the flat run fee is charged only when
Instagram actually answered, because that fee buys a *check* — a run that checked nothing pays
nothing.

**Why is the coverage percentage never 100%?**
Because it is measured against Instagram's own comment count, and that count includes the threaded
replies this route cannot return. A fully-read thread lands at 64%–83%. We show you the real number
rather than rounding it up.

**Does it work at scale?**
Pagination inside one post is sequential (each page needs the previous page's cursor), so scale
comes from running posts in parallel — up to 8 at a time. 8 posts and 1,200 comments took 27
seconds.

**How current is the data?**
It is read live from Instagram at run time. There is no cache and no stored copy.

**What is `POST_STATUS`?**
A record in the run's key-value store holding the authoritative per-post ledger — pages read,
comments scanned, rows returned, what stopped the read and the coverage percentage. It is the one
place that is always correct, including on very large threads whose rows had to be streamed out
before the crawl finished.

***

### ⚖️ Legal, terms and personal data — please read

**Instagram's `robots.txt`.** Its notice and its wildcard rule, quoted verbatim (fetched
2026-09-07):

```
## Notice: Collection of data on Instagram through automated means is
## prohibited unless you have express written permission from Instagram
## and may only be conducted for the limited purpose contained in said
## permission.
## All authorized user-agents listed on this page must comply with Meta's
## Automated Data Collection Terms available at:
## https://www.facebook.com/legal/automated_data_collection_terms

User-agent: *
Disallow: /
```

We are showing you that in full so you can make your own decision with the facts in front of you.
Instagram's Terms of Use and Meta's Automated Data Collection Terms apply to what you do with this
Actor, and complying with them is your responsibility, not ours.

**This is personal data.** Every row is an identifiable person: a username, a numeric account id, a
profile photo URL, and whatever they typed. Under GDPR, CCPA and comparable laws you are the
controller of anything you collect here. That means you need a lawful basis, you should keep only
what you need for as long as you need it, and you must honour deletion requests. The
`emailsInText` field makes this sharper, not softer — an email address a person typed into a public
comment is still their personal data.

**Only public posts.** This Actor reads what a logged-out visitor can see. It does not log in, does
not use anyone's session, does not touch private accounts, and cannot see anything behind a follow
request.

**Profile picture URLs expire.** They are signed CDN links. Fetch the image if you need it; do not
store the URL and expect it to work next month.

**Nothing here is legal advice.** If your use case is sensitive — profiling, marketing to
individuals, anything involving minors — talk to your own counsel first.

***

### 🐛 Something wrong?

Open an issue on the Actor page with the run id. Every run writes a `POST_STATUS` record and a
status message that names exactly what stopped it, which usually answers the question on its own.

# Actor input Schema

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

The posts whose comments you want. Paste any public post, reel or IGTV URL — https://www.instagram.com/p/DZ5T2XPllXv/, https://www.instagram.com/reel/<code>/, https://www.instagram.com/tv/<code>/ — with or without tracking parameters. All three media types were verified live (image, carousel and video/reel posts all resolve). This Actor reads ONE post's comment stream per URL: it does not crawl a profile, a hashtag or an explore feed, and instagram.com/share/ redirect links are rejected rather than guessed at. Leave empty and it runs the documented sample so an empty input still returns rows.

## `shortcodes` (type: `array`):

The same thing without the URL: DZ5T2XPllXv, DWy2NsDD7CE. Handy when your post ids come out of a spreadsheet or another Actor. Mixed with the URL list above; duplicates are collapsed.

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

Stop after this many delivered comments on each post. 0 = the whole top-level thread. Instagram serves ~13-15 comments per call whatever page size is asked for (measured: first=12, 24, 50 and 100 all returned 14 edges), so a 2,118-comment post is 148 calls / ~106 s and a 397-comment post is 28 calls / ~19 s. Keep a cap on unless you mean to pull an entire thread.

## `maxCommentsScannedPerPost` (type: `integer`):

The WORK bound, as opposed to the delivery bound above. A filter or a date window drops a comment AFTER it has been read, so 'give me the 10 comments mentioning "discount"' can mean reading a 79,000-comment thread to find them. This caps how many comments the Actor will READ per post. 0 = automatic (recommended): no budget at all when nothing can drop a row, and otherwise 100 comments read per row you asked to be delivered — floor 1,000, ceiling 20,000. Measured A/B on one input (a filter matching nothing on a 2,118-comment thread, at maxCommentsPerPost: 10 so the budget is its 1,000 floor, both builds run at the same moment): without the budget 2,119 comments were read over 148 calls in 113 s and 0 delivered; with it, 1,009 read over 72 calls in 38 s and 0 delivered — same result, 2.7x less work. The budget is proportional to the rows you asked for, so it bites hardest where the waste is biggest and not at all on a thread smaller than it: re-measured on that same input at the DEFAULT maxCommentsPerPost: 50 the budget is 5,000, it never trips on that 2,118-comment thread, and the whole thread is read (2,119 comments, 0 rows, $0 charged, scanBudgetReached false — run DFYxmxcpJdiOKsghL, build 0.2.12). Set a number to override; set 500000 for effectively no budget. It is checked at a page boundary, so it can overshoot by one page (~13-15 comments) in your favour. Comments read are NEVER billed — only comments delivered are — and when this stops a post the run says so, names the filter and names this field.

## `maxComments` (type: `integer`):

Overall ceiling across every post in this run. 0 = no overall ceiling. Useful when you paste 200 URLs and want to bound the bill rather than each post.

## `commentsNewerThan` (type: `string`):

THE POINT OF THIS ACTOR. A date (2026-09-01), a timestamp (2026-09-01T12:00:00Z) or a span ("7 days", "48 hours", "2 weeks"). Instagram serves this comment stream strictly newest-first — 0 inversions of created\_at across 3,189 comments on 18 posts, measured 2026-09-07 — so the crawl STOPS at the boundary instead of pulling the thread and filtering afterwards. On a 79,000-comment post, "yesterday" costs you a handful of calls and a handful of rows, not 79,000 of each. Comments older than the bound are never delivered and never billed.

## `commentsOlderThan` (type: `string`):

The other edge of the window, same formats. Comments newer than this sit at the head of the stream and are skipped without being delivered or billed. Use both bounds to pull, say, only the comments posted during a giveaway window.

## `dateBoundGracePages` (type: `integer`):

How many extra pages to read after the stream first crosses the "newer than" boundary, before stopping. Reverse-chronological order measured clean (0 inversions in 3,189 comments), so 1 is plenty of insurance; raise it if you would rather pay for a few more calls than risk a straggler. 0 stops immediately at the boundary.

## `monitorMode` (type: `boolean`):

Schedule this Actor and each run delivers ONLY the comments posted since the previous run. The last-seen comment id per post is kept in a NAMED key-value store ('instagram-comments-monitor-state'), so paging stops exactly at it — no dedupe heuristics, no re-billing of comments you already have. Adds a flat monitor fee per run (see Pricing), charged only if Instagram actually answered.

## `monitorStateKey` (type: `string`):

Optional label that separates one watch from another on the SAME post — e.g. run a 'support' watch and a 'giveaway' watch over the same URL without either resetting the other. Leave empty for the default watch.

## `monitorSeedComments` (type: `integer`):

On the very first run for a post there is no last-seen id, so every comment would read as new. This bounds that seed run. 0 = record the baseline and deliver nothing, so your first real delivery is genuinely new comments only.

## `alertOnNewComment` (type: `boolean`):

In Monitor mode, push each new comment to the webhook / Slack / email sinks below. Alerts are billed only when a delivery actually succeeds, and never fire outside Monitor mode.

## `webhookUrl` (type: `string`):

POSTed one JSON body per new comment: { event: 'new-instagram-comment', comment: {…the full row…}, scrapedAt }.

## `slackWebhookUrl` (type: `string`):

A Slack incoming webhook. Each new comment arrives as a card with the commenter, the verified badge, the like count and a deep link to the comment.

## `emailRecipients` (type: `array`):

One digest email per monitor run listing the new comments, sent through apify/send-mail. Billed as one alert per recipient, and ONLY when that send-mail run reports SUCCEEDED — a refused or failed send is never charged. Note apify/send-mail's own limits: on the Apify free plan it accepts only your own account address, and up to 20 recipients on a paid plan.

## `commentTypes` (type: `array`):

GIF comments come back with text:"" and a Giphy payload — 268 of 3,189 comments measured (8.4%), and every single one carried a live GIF URL. Most scrapers in this lane ship them as blank rows; this one types them and fills gifUrl / gifId. 'empty' is the rare row with neither text nor a GIF (6 of 3,189). Rows dropped here are never billed.

## `minLikeCount` (type: `integer`):

Keep only comments with at least this many likes. 81% of measured comments had zero likes (609 of 3,189 had at least one), so even minLikeCount 1 is a strong signal filter.

## `onlyVerifiedCommenters` (type: `boolean`):

Keep only comments from blue-badge accounts. Measured 139 of 3,189 comments (4.4%) — a small, high-signal slice for PR and influencer work.

## `textIncludes` (type: `array`):

Case-insensitive substrings. A comment is kept if it contains ANY of them. Leave empty for no keyword filter.

## `textExcludes` (type: `array`):

Case-insensitive substrings that disqualify a comment. Handy for stripping emoji-spam or a giveaway hashtag.

## `requireEmailInText` (type: `boolean`):

Keep only comments where the commenter typed an email address. This reads what is IN the comment — it is not an email lookup or enrichment of any kind.

## `requireLinkInText` (type: `boolean`):

Keep only comments containing an http(s):// or www. link.

## `excludePostOwnerComments` (type: `boolean`):

Removes the creator's own replies from the thread. Matched on the numeric account id, not the username, so it works even without the post-context fetch.

## `onlyCommenters` (type: `array`):

Usernames to keep (with or without the @). Everything else is dropped before delivery.

## `excludeCommenters` (type: `array`):

Usernames to drop — your own brand account, known bots, a repeat spammer.

## `includePostContext` (type: `boolean`):

One extra fetch of the post's own page (736-947 KB measured, ~1.2 s) per post, which adds the owner's username, Instagram's OWN comment count, the like count, the caption, the posted-at date — and the coverage ledger: what share of the thread this run actually read. Nobody else in this lane tells you that. Billed once per post, only when the post produced rows. In Monitor mode leave it OFF: it re-fetches ~750 KB and a charge per post on every scheduled tick for information that barely changes.

## `includeRawComment` (type: `boolean`):

Adds a rawComment column holding Instagram's untouched node object, for anyone who wants a field this Actor does not map. Roughly doubles the dataset size.

## `deduplicateComments` (type: `boolean`):

On by default. 0 duplicate comment ids were measured across 3,189 comments and 228 paginated calls, so this is a belt-and-braces guarantee rather than a fix for a known problem — and de-duplication runs BEFORE billing, so a duplicate could never be charged twice.

## `resumeMode` (type: `boolean`):

For deep backfills of huge threads. The pagination cursor for each post is stored in a NAMED key-value store ('instagram-comments-resume-state'), so the next run with this switched on continues where the last one stopped instead of paying to re-read the head of the thread. Split a 79,000-comment post over several cheap runs. Ignored when Monitor mode is on — the two pull in opposite directions.

## `resetResumeState` (type: `boolean`):

Clears the saved pagination cursors so the next resumable run starts again from the newest comment.

## `docIdRecovery` (type: `boolean`):

This route rides a Meta persisted-query id, and Meta rotates those on deploys. If EVERY post — including this Actor's own known-live sample — comes back empty, the Actor re-mints its session and then sweeps the post page's JavaScript bundles to recover the new ids and caches them, instead of exiting successfully with zero rows. Costs nothing on a normal run. Leave on unless you are debugging.

## `maxPagesPerPost` (type: `integer`):

Hard ceiling on paginated calls per post. 0 = no ceiling. One page is ~13-15 comments regardless of the page size asked for.

## `maxSecondsPerPost` (type: `integer`):

Give up on one post after this long and move to the next, so a single enormous thread cannot eat a whole run. 0 = no per-post limit. The reason is recorded as truncationReason on the rows and in the run's POST\_STATUS record.

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

Pagination inside ONE post is strictly sequential (each page needs the previous page's cursor), so this parallelises across posts. 3 is a good default; 228 consecutive calls on one minted session measured 100% HTTP 200.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. Comment calls averaged 932 ms across 228 calls through an Apify datacenter proxy (710 ms on the sequential deep-pagination run), so the default is generous.

## `maxRequestRetries` (type: `integer`):

Retries on a timeout, a 429 or a 5xx. One transient proxy ECONNRESET was seen in ~350 calls — that is a retry, not a block.

## `maxBufferedCommentsPerPost` (type: `integer`):

Rows are held until a post's crawl finishes so the completeness columns on every row are final. Past this many they are streamed out instead, and topLevelComplete / truncationReason / postCommentsScanned / topLevelCoveragePct are null on those rows rather than guessed — the run's POST\_STATUS key-value record then holds the authoritative ledger. Only bites on threads of tens of thousands of comments.

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

Apify Proxy, automatic (datacenter) group. Measured 2026-09-07 on the comment endpoint: datacenter 228/228 HTTP 200 at ~932 ms average, residential US 10/10 at ~1,049 ms. Datacenter is both faster and cheaper here, so it is the default; switch to RESIDENTIAL only if you start seeing 429s.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.instagram.com/p/DZ5T2XPllXv/"
    },
    {
      "url": "https://www.instagram.com/p/DWy2NsDD7CE/"
    },
    {
      "url": "https://www.instagram.com/p/Db9IVmrDvQ4/"
    }
  ],
  "shortcodes": [],
  "maxCommentsPerPost": 25,
  "maxCommentsScannedPerPost": 0,
  "maxComments": 0,
  "commentsNewerThan": "7 days",
  "commentsOlderThan": "2026-09-01",
  "dateBoundGracePages": 1,
  "monitorMode": false,
  "monitorStateKey": "giveaway-oct",
  "monitorSeedComments": 25,
  "alertOnNewComment": true,
  "commentTypes": [
    "text",
    "gif",
    "empty"
  ],
  "minLikeCount": 0,
  "onlyVerifiedCommenters": false,
  "textIncludes": [
    "shipping",
    "price",
    "where can i buy"
  ],
  "requireEmailInText": false,
  "requireLinkInText": false,
  "excludePostOwnerComments": false,
  "includePostContext": true,
  "includeRawComment": false,
  "deduplicateComments": true,
  "resumeMode": false,
  "resetResumeState": false,
  "docIdRecovery": true,
  "maxPagesPerPost": 0,
  "maxSecondsPerPost": 0,
  "maxConcurrency": 3,
  "requestTimeoutSecs": 45,
  "maxRequestRetries": 3,
  "maxBufferedCommentsPerPost": 20000,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

The dataset of scraped comments — text or GIF, likes, timestamp, deep link, and the commenter's username, numeric id, verified badge and profile URL, with hashtags, @mentions, links and any email address the commenter typed pulled out of the text.

## `postStatus` (type: `string`):

POST\_STATUS: for every post in the run — pages read, top-level comments scanned, rows delivered, whether the whole top-level thread was read, what stopped it, and what share of Instagram's own comment count that represents.

# 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": [
        {
            "url": "https://www.instagram.com/p/DZ5T2XPllXv/"
        },
        {
            "url": "https://www.instagram.com/p/DWy2NsDD7CE/"
        },
        {
            "url": "https://www.instagram.com/p/Db9IVmrDvQ4/"
        }
    ],
    "shortcodes": [],
    "maxCommentsPerPost": 25,
    "includePostContext": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/instagram-comments-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": [
        { "url": "https://www.instagram.com/p/DZ5T2XPllXv/" },
        { "url": "https://www.instagram.com/p/DWy2NsDD7CE/" },
        { "url": "https://www.instagram.com/p/Db9IVmrDvQ4/" },
    ],
    "shortcodes": [],
    "maxCommentsPerPost": 25,
    "includePostContext": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/instagram-comments-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 '{
  "startUrls": [
    {
      "url": "https://www.instagram.com/p/DZ5T2XPllXv/"
    },
    {
      "url": "https://www.instagram.com/p/DWy2NsDD7CE/"
    },
    {
      "url": "https://www.instagram.com/p/Db9IVmrDvQ4/"
    }
  ],
  "shortcodes": [],
  "maxCommentsPerPost": 25,
  "includePostContext": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/instagram-comments-scraper --silent --output-dataset

```

## MCP server setup

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