# Instagram Likes Scraper — Post Likers, Commenters & Emails (`afanasenko/instagram-post-likers-scraper`) Actor

Scrape Instagram post likers and commenters — the accounts that engaged with any post, each returned as a full profile with emails, bio links, follower count, engagement rate and business category. Paste a post or reel link. No login needed.

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

## Pricing

$10.00 / 1,000 profiles

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

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

## What's an Apify Actor?

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 Likes Scraper — Post Likers, Commenters & Emails

[![Apify Actor](https://apify.com/actor-badge?actor=afanasenko/instagram-post-likers-scraper)](https://apify.com/afanasenko/instagram-post-likers-scraper)

> **Paste an Instagram post or reel link and get back the accounts that liked or commented on it — each one as a full profile with email, phone, bio links, follower count, engagement rate and business category. These are the warmest leads on Instagram, because every row demonstrably engaged with something.**

Everyone else's audience is a guess. A post's likers and commenters are not: they saw a specific piece of content and reacted to it. Give the actor the post — yours, a competitor's, an industry account's — and it turns that reaction into a contactable, filtered list. No login, no cookies, no Instagram account of yours involved.

### 🎯 What it does

You give it post links. Post or reel, one per line, up to 50 in a run:

```
https://www.instagram.com/p/CA2aJYrg6cZ/
https://www.instagram.com/reel/DBz1qXYtR3m/
```

Every account that liked or commented becomes a candidate. **Engagement Type** decides which:

- **Likers and commenters** (default) — both, merged into one deduplicated list.
- **Likers only** — the widest top of funnel.
- **Commenters only** — the smallest and warmest group. Someone who typed a sentence invested more than someone who tapped a heart.

Candidates are deduplicated across all your posts, so an account that liked three of them is one profile you pay for once — and its `Posts Engaged` count is 3, which is exactly how you rank the list.

For every account that comes back you get:

- **Engagement with your posts** — Liked Posts, Comments Left, Posts Engaged, and which posts (Source).
- **Identity** — handle (Account URL), Full Name, Profile Picture, verified flag.
- **Audience** — Followers Count, Following Count, Total Posts, Posts per Month, Last Post age in days.
- **Reels analytics** — Reels Count, Last Reel age, Median Views, Views/Followers ratio.
- **Engagement** — Avg Likes, Avg Comments, Median Engagement Rate, Quality flag (Good / Poor).
- **Contact** — Email, Email Source, Phone, External URL, Physical Address.
- **Classification** — Business Category, Detected Language, Account Type.
- **Optional post sample** — captions of the 8 most recent posts, plus contacts parsed out of them.

Then **your filters** run — follower band, engagement floor, language, business category, contact-channel presence, posting cadence, verification — and only the matches are saved.

#### How much of a post can actually be read

This is the one thing worth knowing before you pick a post, and the actor tells you the answer for every run.

**Likers do not paginate.** Instagram serves a post's likers as a single capped sample — measured at 999 to 1 896 accounts per post — with no cursor to page past it. On a post with 3 000 likes that is a third to two thirds of them; on a post with 87 000 it is about 1–2 %. Nothing can change that, and any tool claiming a complete liker list for a viral post is not describing Instagram.

**And most of a popular post's likers are private.** Measured 2026-07-28 across posts spanning 1.6k–87k likes: about **72 % of the accounts in a liker sample are private**, and a private account publishes no profile data, so it is skipped before analysis and never billed. Read together with the cap: a 3 000-like post sampled at ~1 000 likers yields roughly 280 accounts you can actually receive.

**Comments do paginate**, and the actor reads up to 10 pages per post — about 150 comments. Beyond that it stops and says so.

Both facts are reported, never smoothed over: the run writes an `ENGAGEMENT_COVERAGE` record naming, per post, how many likers came back against the declared like count, how many comment pages were read, and why reading stopped. If anything was cut short, the run's `USER_MESSAGE` says which post and by how much — as the headline when nothing more urgent happened, and appended behind the warning when something did. The rows you get are always real observations — a capped sample leaves accounts out, it never invents one.

**What follows from it:** if you need a set you can defend as complete, use **Commenters only** on posts with under ~150 comments — with one honest caveat: reading can also stop because the source stops answering partway, which is not our cap and which the run reports separately (`commentsStoppedBy` is `end`, `cap` or `error`). If you want reach, use likers and accept a sample.

### ⚡ Quick start

A minimal input that works on the **free plan** (5 accounts, no setup):

```json
{
  "postUrls": ["https://www.instagram.com/p/CA2aJYrg6cZ/"],
  "engagementType": "both",
  "extractEmail": true
}
```

Leave **Post Links** empty and the run does a small capped demo on a well-known public account's newest post instead of failing, so you can see the output shape before deciding what to ask for.

### 📦 Output sample

Every analyzed account is pushed to the dataset as one item. Here is a real row from a real run — the input was one post with 1 571 comments, read as commenters (Profile Picture URL truncated for readability):

```json
{
  "Account": "https://instagram.com/moscow_dillers",
  "Source": "Post: Dbru79IAdH-",
  "Liked Posts": 0,
  "Comments Left": 1,
  "Posts Engaged": 1,
  "Full Name": "moscow dillers",
  "Followers Count": 4396518,
  "Following Count": 2135,
  "Biography": "BLICK — an ecosystem for AI media and growth.\nWe make people and brands popular through influence, scale, and technology.\nJoin",
  "Email": "N/A",
  "Email Source": "N/A",
  "External URL": "N/A",
  "Category": "Artist",
  "Profile Picture": "https://scontent-iad3-2.cdninstagram.com/v/t51.82787-19/788948610_…jpg",
  "Reels Count": 3,
  "Last Reel (Days Ago)": 125,
  "Median Views": 83718,
  "Avg Likes": 65840,
  "Avg Comments": 135,
  "Views/Followers Ratio": "1.90%",
  "Posts in Last 30 Days": 5,
  "Posts per Month": 5,
  "Total Posts": 12,
  "Detected Language": "English",
  "Last Post Within (Days)": 1,
  "Median ER": "1.29%",
  "Quality": "Good",
  "Analysis Status": "analyzed",
  "Why Not Analyzed": "N/A"
}
```

A column whose value could not be determined comes back as `"N/A"` rather than vanishing. Columns for extractions you did not switch on — Phone, Address, the eight Post captions — are omitted entirely, which is why they are absent from the sample above: `extractPhoneNumber` and `extractPosts` are off by default.

**The results also tell you what was left behind.** Any row whose `Analysis Status` is `not_analyzed` is not an account — it is the run telling you about something it did not deliver, with the reason in plain words in `Why Not Analyzed`. None of these rows is ever charged, and they come after your results.

- **A post link that could not be read — on every plan.** One row per link you pasted that the run could not open, naming the link in `Source` and saying which of three things happened: it was never a post link, so nothing was looked up; it is a real post that is deleted or inside a private account; or it hit a temporary error. Links past the 50-per-run ceiling are reported the same way. No link you paste disappears without a word.
- **Free plan — accounts the cap held back.** One row per post that still had accounts left when the free plan stopped the search, with how many more there were.

Both kinds are capped at 25 rows per run, with the true total in the sentence. The free-plan rows never appear on a paid run; the unread-link rows appear on any plan, because a link you pasted and did not get back is a fact about your run, not about your plan. On these rows a blank cell is whatever the column accepts — `"N/A"` in a text column, `null` in a numeric one.

### 🧭 When to use it

- **Warm-lead sourcing.** Take a post that already resonated with your audience — yours or a competitor's — and turn the people who reacted to it into a contactable list.
- **Competitor audience mining.** The accounts engaging with a rival's launch post are, by definition, in-market for what you both sell.
- **Event and campaign follow-up.** After a launch, a giveaway or a collaboration, retrieve everyone who engaged and enrich them with contact details in one pass.
- **Influencer vetting.** Read a creator's post and look at who engages: real accounts with real audiences, or a wall of empty profiles. The Quality flag and follower counts show it immediately.
- **Community research.** Comments on a big industry post are a map of who is active in a niche and what they call themselves.

### 🛠 Input

The input form is grouped into four sections:

| Section | Purpose |
|---|---|
| **🎯 Posts to read** | The post links, which kind of engagement counts, and the cost ceiling. |
| **⚙️ Data Extraction Options** | Which enrichments to run (email, phone, posts, engagement). Off = skip the call → faster and cheaper. |
| **🔬 Advanced Filtering (Optional)** | 20+ filters that drop non-matching accounts. |
| **▶️ Run & Storage Options** | Resume from checkpoint, offline re-filter mode, exclusion list. |

The fields that matter most:

- **`postUrls`** — post or reel links, one per line, up to 50 per run. Copy them with Instagram's own Share → Copy link; a shortened or redirected link may not carry the post ID.
- **`engagementType`** — `both` (default), `likers`, or `commenters`.
- **`maxCountEngagers`** — hard cost ceiling in accounts. Set it to your **spend budget × 100** (e.g. `1000` = $10).
- **`excludeAccounts`** — accounts you already have. They are skipped **before** the charge, so a re-run over the same post costs you only the new people.
- **`minFollowers` / `maxFollowers`** — the most-used filter, set as a band.
- **`minEngagementRate`** — a common floor is 1–3 %.
- **`analyzeQuality`** — on by default; computes Engagement Rate and the Quality flag.
- **`extractPosts`** — fetch the 8 most recent captions and put them in the output. Required only for `searchContactsInPosts`. The time-based filters (`lastPostDays`, `minPostsInPeriod`) and `keywordLocation=posts` fetch what they need on their own, so switching this on for them only buys an extra upstream call per profile.
- **`contactInfoType`** — keep only accounts that have the contact channel you will actually use.

### 💰 Pricing

The actor uses Apify's **Pay Per Event** model with **one single event**:

| Event | Price | Triggered |
|---|---|---|
| **Profile** | **$0.01** | Once per engager that is successfully retrieved and analyzed, whether or not it passes your filters. |

Not charged: accounts that could not be read (not found, private, failed), private accounts skipped before analysis, and anything on your `excludeAccounts` list. On your **first run** you are also not charged for profiles your own filters rejected, up to 100 — so the run where you are most likely to over-filter is the one where over-filtering costs nothing. You are billed per account, not per API call and not per like, so `maxCountEngagers` translates directly into your maximum spend.

#### What filters do to your bill

**Filters run after the fetch.** An account has to be read before the actor can tell whether it matches your follower band or language, so a narrow filter stack costs the same as a wide one and returns fewer rows.

That is not a small effect here, and the size of it is measured rather than estimated. Across **all** runs of this mode on the parent actor — 67 runs, 4 421 charged profiles — **71.8 % of charged profiles were discarded by a filter**. Split by engagement type: **86.2 % for commenters** (1 491 charged profiles, so this figure is solid) and **25.0 % for likers** — but that likers cell rests on **4 runs and 20 charged profiles**, which is far too thin to plan against. Treat the commenters number as measured and the likers number as a hint.

What to do with that:

1. Run **filterless first** and look at what the post's audience actually is.
2. Turn on **Enable Offline Mode** to try any filter combination against the accounts you already paid for. Re-filtering makes no requests and charges nothing.
3. Only then set the filters for the next run.

The actor writes a `FILTER_BURN_PREDICTION` record before fetching and warns you in the run log when your filter stack looks too narrow for the pool it is about to read.

### 🆓 Free vs paid plan

| | Free plan | Paid plan |
|---|---|---|
| Accounts per run | 5 | unlimited |
| Runs per calendar month | 15 | unlimited |
| Post links per run | up to 50 | up to 50 |
| Filters (followers, language, engagement, …) | ✓ | ✓ |
| Reels analytics (Median Views, Views/Followers) | ✓ | ✓ |
| Offline re-filtering | ✓ | ✓ |
| Resume from checkpoint | ✓ | ✓ |

Demo runs (empty Post Links) and offline re-filter runs do not consume the monthly allowance. A run that **ends by itself** before fetching anything gives its slot back — a configuration error, say. A run **you abort** keeps its slot: the allowance is reserved before the first fetch precisely so that stopping a run mid-flight is not a way around it.

### 🧪 Recipes

**The whole audience of one post, unfiltered**

```json
{
  "postUrls": ["https://www.instagram.com/p/CA2aJYrg6cZ/"],
  "engagementType": "both",
  "maxCountEngagers": 500
}
```

**Only people who wrote something, with an email**

```json
{
  "postUrls": ["https://www.instagram.com/p/CA2aJYrg6cZ/"],
  "engagementType": "commenters",
  "extractEmail": true,
  "contactInfoType": "emailOnly",
  "maxCountEngagers": 300
}
```

**Micro-influencers among a competitor's engagers**

```json
{
  "postUrls": ["https://www.instagram.com/p/CA2aJYrg6cZ/"],
  "engagementType": "likers",
  "minFollowers": 3000,
  "maxFollowers": 100000,
  "minEngagementRate": 2,
  "analyzeQuality": true,
  "maxCountEngagers": 1000
}
```

**Your last five posts, ranked by how many they engaged with**

```json
{
  "postUrls": [
    "https://www.instagram.com/p/AAAAAAAAAAA/",
    "https://www.instagram.com/p/BBBBBBBBBBB/",
    "https://www.instagram.com/p/CCCCCCCCCCC/",
    "https://www.instagram.com/p/DDDDDDDDDDD/",
    "https://www.instagram.com/p/EEEEEEEEEEE/"
  ],
  "engagementType": "both",
  "maxCountEngagers": 2000
}
```

Then sort the dataset by **Posts Engaged** descending. An account that engaged with four of your five posts is a different kind of lead from one that engaged with a single post.

**A second pass that skips who you already have**

```json
{
  "postUrls": ["https://www.instagram.com/p/FFFFFFFFFFF/"],
  "engagementType": "both",
  "excludeAccounts": ["handle_you_already_contacted", "another_one"],
  "maxCountEngagers": 500
}
```

### 🔁 Resume / checkpoint

Long runs can be interrupted (timeout, manual stop, abort). The actor writes a checkpoint after every batch. To resume, run it again with **`clearSavedData`** unchecked — accounts already analyzed are not re-billed.

### 📡 Live status & storage records

| Storage key | Type | What it holds |
|---|---|---|
| `status.html` | HTML page | Live progress, cost, ETA. Open it from the run's Storage tab. |
| `ENGAGEMENT_COVERAGE` | JSON | Per post: likers returned against the declared like count, comment pages read, and whether each list was read in full. The run's own statement of how much it could see. |
| `RUN_SUMMARY` | JSON | Aggregate run statistics — counts, plan, filter breakdown, upstream request totals, and the pre-flight volume estimate. |
| `SKIPPED_ACCOUNTS` | JSON | Every account skipped, with reason and category (filter / not\_found / private / error). |
| `USER_MESSAGE` | JSON | The one thing this run most needs to tell you — a coverage shortfall, a zero-result diagnosis, a demo notice, or a free-plan cap. |
| `FILTER_BURN_PREDICTION` | JSON | Written whenever a post-fetch filter is active: how much of the fetch your filter stack is likely to reject. |
| `API_USAGE` | JSON | Upstream request count for this run. |

### 🤖 Programmatic / API use

**REST API:**

```bash
curl -X POST "https://api.apify.com/v2/acts/afanasenko~instagram-post-likers-scraper/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "postUrls": ["https://www.instagram.com/p/CA2aJYrg6cZ/"],
    "engagementType": "both",
    "maxCountEngagers": 200,
    "minFollowers": 1000,
    "extractEmail": true
  }'
```

After the run succeeds, read rows from its default dataset; run-level statistics live in the `RUN_SUMMARY` record of the same run, and read completeness in `ENGAGEMENT_COVERAGE`.

**MCP / agentic clients (Claude, ChatGPT, Cursor, LangGraph, OpenAI Agents SDK, Mastra, …):**

The actor is exposed as a tool through Apify's [MCP server](https://docs.apify.com/platform/integrations/mcp) at `mcp.apify.com`. To mount it in your agent as a dedicated tool in one command:

```bash
apify mcp install claude-code --tools afanasenko/instagram-post-likers-scraper
```

Then ask in natural language — *"find everyone who commented on this post and has over 5 000 followers and a public email"* — and the agent fills the input schema itself.

### 💡 Tips & best practices

- **Pick a post with real engagement.** A few hundred likes is enough; a handful is not. The single biggest cause of a thin run is a quiet post, not a wrong setting.
- **Start with no filters.** Over-filtering is the most common cause of an empty run, and here it is expensive: 72 % of charged profiles are rejected by filters on average. Harvest filterless once, then use **Enable Offline Mode** to try filter combinations against what you already bought — free, no requests.
- **Use commenters when you need certainty, likers when you need reach.** Comments are read page by page and are complete under ~150 per post; likers are a capped sample on any popular post.
- **Give it several posts at once.** Deduplication is across all of them, so you pay once per account, and `Posts Engaged` becomes a ranking column you cannot get any other way.
- **Feed `excludeAccounts` from your last export.** Skips happen before the charge, so a weekly re-run over the same account costs you only the new people.
- **Check `ENGAGEMENT_COVERAGE` before you conclude anything about size.** "Only 1 000 likers came back" on an 80 000-like post is Instagram's cap, not an empty audience.

### ❓ FAQ

**Q: Can I get the complete list of everyone who liked a post?**
A: Not for a popular post, and no tool can. Instagram serves likers as one capped sample of roughly a thousand accounts with no way to page past it. The actor reports exactly what fraction it saw in `ENGAGEMENT_COVERAGE` rather than implying the list is whole. Comments do paginate, so a post with fewer than ~150 comments can be read in full.

**Q: Do I need an Instagram account, login, or cookies?**
A: No. You provide public post links and get public profile data back. No credentials of any kind are involved, so there is no risk to any Instagram account of yours.

**Q: Can I use a competitor's post?**
A: Yes, as long as the post is public. The actor reads the same page any logged-out visitor sees.

**Q: Why were private accounts not returned?**
A: Instagram publishes no profile data for them, so they are skipped before analysis and never billed. The count is in `RUN_SUMMARY` and each one is listed in `SKIPPED_ACCOUNTS`.

**Q: Are filtered-out accounts charged?**
A: Yes — an account must be fetched before it can be filtered, so it counts as one paid event even if a filter then rejects it. That is why the actor writes `FILTER_BURN_PREDICTION` before fetching and warns you when your filter stack looks too narrow. Use Offline Mode to re-filter for free.

**With one exception, on your very first run.** A first-time user is not billed for profiles their own filters rejected, up to 100 of them. So you can set the filters you actually want, see the real yield, and only pay for rows you keep — the run where you are most likely to over-filter is the one where over-filtering is free.

**Q: What if I paste the same account's post twice, or two posts they both engaged with?**
A: Candidates are deduplicated across all submitted posts. You are charged once per account, and `Posts Engaged` tells you how many of your posts it reacted to.

**Q: What does `Comments Left` count?**
A: Comment events, not distinct posts. Somebody who left three comments on one post has `Comments Left: 3` and `Posts Engaged: 1`.

**Q: How is engagement rate calculated?**
A: For each of the account's own recent posts, `(likes + comments) / followers`, then the median across the sample. The median resists viral outliers better than the mean.

**Q: Why are my Reels metrics blank?**
A: Not because of your plan — since 0.1.65 the free plan runs every filter and every Reels metric exactly like paid. Blank Reels columns mean Instagram returned no clip data for that account: it has no Reels, or the media fetch failed for it. Those accounts are still charged, because the profile itself was retrieved.

**Q: My post link was rejected — why?**
A: Two different reasons, and the run tells you which. Either the entry was never a post link (a profile link or an `@handle` points at an account, not a post — nothing was looked up and nothing was charged), or it was a post link the actor could not open (deleted, or inside a private account).

**Q: Which input field names does the API expect?**
A: `postUrls` (array of strings), `engagementType` (`both` / `likers` / `commenters`), `maxCountEngagers` (number). Common wrong guesses — `posts`, `urls`, `maxItems`, `maxResults`, or the seed fields from the six-mode Profile Scraper — are **not** recognized; the run log and the run report name them back to you with the field you probably meant.

**Q: Can I schedule it?**
A: Yes, on any plan including free. Save your input as a Task in Apify Console, then add a Schedule to the Task (⋮ menu → Schedule). Pair it with `excludeAccounts` so each run returns only people you have not seen.

### 🔗 Other Instagram scrapers (same author)

Pick the one that matches what you start with:

| You start with… | You want to find… | Use this actor |
|---|---|---|
| A post or reel link | The accounts that liked or commented on it, enriched and filtered | **This actor** |
| A keyword or a hashtag | The accounts in that niche, enriched and filtered | [Instagram Influencer Search](https://apify.com/afanasenko/instagram-influencer-search) |
| 1–5 seed handles you like | Similar / suggested accounts in the same niche | [Instagram Related Profiles Scraper](https://apify.com/afanasenko/instagram-related-profiles-scraper) |
| One known account | Its full **followers** or **following** list, with filters and enrichment | [Instagram Followers & Following Extractor](https://apify.com/afanasenko/instagram-followers-scraper) |
| One known account | **Track** who follows / unfollows it over time (scheduled) | [Instagram Follower Tracker](https://apify.com/afanasenko/instagram-follower-tracker) |
| Anything else (handles, post links, locations, keywords) | All of the above plus five other discovery modes in one tool | [Instagram Profile Scraper](https://apify.com/afanasenko/instagram-profile-scraper) |
| A reel link or a creator handle | What their reels **say and show** — spoken transcript, on-screen text, hook and structure | [Instagram Reel Script Extractor](https://apify.com/afanasenko/instagram-reel-script-extractor) |
| An AI agent (Claude, Cursor, ChatGPT) | The same Instagram data from inside your agent — MCP tools it picks by intent | [Instagram Profile MCP Server](https://apify.com/afanasenko/instagram-profile-scraper-mcp-server) |

### 🛟 Support & feedback

- **Open an issue** on the actor's Issues tab in Apify Console — the fastest way to get a fix shipped.
- **Feature requests** in the same tab; we read every one.
- **Ratings & reviews** on the [Apify Store page](https://apify.com/afanasenko/instagram-post-likers-scraper) help other users find the actor.

### ⚖️ Disclaimer

This actor reads **publicly available** Instagram profile data from public posts. It does not bypass authentication, read private profiles, or interact with content on your behalf. Use it in compliance with Instagram's Terms of Service and applicable privacy laws (GDPR, CCPA) — for outreach lists, make sure you have a legitimate interest and a clear unsubscribe path. The contact details returned are what the profile owner chose to publish on Instagram.

# Actor input Schema

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

Instagram post or reel links, one per line — e.g. https://www.instagram.com/p/CA2aJYrg6cZ/. Reel links work too. Every account that liked or commented on those posts becomes a candidate. A link that is not a post link, a deleted post, and a post inside a private account cannot be read — each one comes back as its own free row in the results, naming the link and why, and is also listed in the run's Skipped Accounts record.

## `engagementType` (type: `string`):

Which kind of engagement makes an account a candidate. 'Likers and commenters' merges both into one deduplicated list. 'Commenters only' reads comments page by page, up to 10 pages (about 150 comments) per post; when a post has more, the run says so instead of implying the list is whole. 'Likers only' is the widest top of funnel.

## `maxCountEngagers` (type: `integer`):

Maximum number of unique engagers to analyze — hard cost ceiling, because each analyzed profile is one paid event ($0.01). Counts unique accounts, not likes: someone who liked three of your posts is one profile. Private accounts are skipped before analysis and never counted. Free plan ceiling: 5 profiles. 0 = unlimited. Default 0.

## `extractEmail` (type: `boolean`):

When true, extracts public email addresses from each profile (Instagram Contact button + bio parse). Set false to skip email extraction. Default true. Populates the Email + Email Source columns.

## `extractPhoneNumber` (type: `boolean`):

When true, extracts public phone numbers from each profile (Instagram Contact button + bio parse). Set false to skip. Default false (most lead-gen workflows rely on email/website). Populates the Phone column when enabled.

## `extractWebsiteUrl` (type: `boolean`):

When true, extracts the bio-link URL from each profile. Set false to skip. Default true. Populates the External URL column.

## `extractBusinessCategory` (type: `boolean`):

When true, extracts the Instagram business category for business profiles (e.g. «Athlete», «Beauty, Cosmetic & Personal Care»). Set false to skip. Default true. Populates the Category column.

## `extractPhysicalAddress` (type: `boolean`):

When true, extracts the public street address shown on a business profile when disclosed. Default false. Populates the Address column.

## `analyzeQuality` (type: `boolean`):

When true, computes Median Engagement Rate and a coarse Quality score (Good/Poor) from recent posts, plus Reels analytics (median views, view/follower ratio) on every plan. Requires an extra API call per profile. Default true. Populates Median ER, Quality, Avg Likes, Avg Comments, Posts per Month columns.

## `minEngagementRate` (type: `integer`):

Minimum median engagement rate (%) — profiles below this are skipped. 0 = no filter (default). Engagement rate is computed from likes+comments on recent posts vs follower count; requires analyzeQuality=true.

## `extractPosts` (type: `boolean`):

When true, extracts caption text from the 8 most recent feed posts. Default false. Populates Post 1..Post 8 columns and is a prerequisite for searchContactsInPosts.

## `searchContactsInPosts` (type: `boolean`):

When true, scans the extracted post captions for emails and phone numbers (e.g. booking@brand.com referenced in a Reel description). Requires extractPosts to also be true. Default false. Populates Email from Posts + Phone from Posts columns.

## `keywords` (type: `array`):

Only process profiles containing **any** of these keywords. Not case-sensitive.

## `keywordLocation` (type: `string`):

Where to search for keywords — `bio_or_name` (default), `bio` only, `name` only, `posts` (recent post captions, requires post data), or `anywhere`. Choose `posts` for high-intent niche matching beyond profile metadata.

## `locationKeywords` (type: `string`):

Keeps a profile only if its public business address or city contains one of your terms — any single match keeps it.
• **Separate terms with a comma.** A space-separated list is read as one long term and almost never matches.
• **Only professional accounts publish an address** — so search for business types ("dental clinic"), not people ("fitness coach"). Used alone, this filter returns nothing on 3 runs out of 4.
• **Write a list, not one city** — "Miami, Fort Lauderdale, Boca Raton, Florida". Terms are OR-ed, so an extra term can only help: lists of six or more delivered four times as often as a single word.
• **Add the region in full, the way Instagram spells it** — the state in the US, Canada, Mexico and India ("Miami, Florida"), the country in English elsewhere ("Berlin, Germany"). The field carries "United Arab Emirates", not "UAE".
• **Copy Instagram's spelling** — accents included ("São Paulo", not "Sao Paulo"), and whole place names: short terms match inside longer words ("us" hits "Houston").
Leave empty for no location filter.

## `profileLanguage` (type: `string`):

Language filter — restrict results to profiles whose bio + post text was detected as this language. «any» (default) bypasses detection. Detection runs on bio + recent post captions via Google CLD3. Closely related languages can be mistaken for one another — Serbian written in Latin letters is usually detected as Croatian, and Malay as Indonesian.

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

Minimum follower count — profiles below this are skipped. 0 = no minimum (default). Filters run after each profile is fetched, so a floor set too high returns an empty run you still paid for. Start at 0, look at the follower counts you actually get back, then raise it.

## `maxFollowers` (type: `integer`):

Maximum follower count — profiles above this are skipped. 0 = no maximum (default). Useful for excluding mega-accounts, but a narrow min-max band is the fastest way to end up with zero rows - widen it if a run comes back empty. As a rule of thumb, keep the maximum at least 10x the minimum: bands tighter than that keep under 10% of the profiles they analyze, and you pay for the rest.

## `lastPostDays` (type: `integer`):

Activity recency filter — only include profiles that posted any feed content within the last X days. 0 = disabled (default). Drops dormant accounts. Requires post data.

## `lastReelDays` (type: `integer`):

Reel-activity recency filter — only include profiles that posted a Reel within the last X days. 0 = disabled (default). Useful for Reels-driven campaigns.

## `minPostsInPeriod` (type: `integer`):

Only process accounts with at least this many posts within the specified period. Set to 0 to disable. **Requires post data.**

## `postsCheckPeriodDays` (type: `string`):

The time period (in days) to check for the minimum number of posts.

## `minMedianViews` (type: `integer`):

Only process accounts where the median view count of their recent posts is greater than this number. Set to 0 to disable.

## `viewFollowerRatioMin` (type: `integer`):

Reject profiles whose median Reel views are below this % of their follower count. 0 = no minimum. For B2B targeting, 5 is a sensible floor.

## `viewFollowerRatioMax` (type: `integer`):

Reject profiles whose median Reel views are above this % of their follower count (filters out viral one-hit accounts). 0 = no maximum. For B2B targeting, 30 is a sensible ceiling.

## `contactInfoType` (type: `string`):

Contact-info filter — keep only profiles that have at least the requested contact data. «any» (default, no filter), «emailOrPhone», «emailOnly», «phoneOnly», «emailAndPhone». Runs independently of the extract-\* flags above. Combine with lead-gen workflows where you must have email or phone.

## `hasWebsite` (type: `boolean`):

When true, only profiles with a website link in their bio pass the filter. Default false. Useful for B2B / lead-gen lists where the link is a qualified-traffic signal.

## `accountType` (type: `string`):

Account-type filter — restrict to «business» profiles, «personal» profiles, or «any» (default, no filter). Most ecommerce / B2B targeting wants «business»; influencer discovery often wants «personal».

## `filterForInfluencers` (type: `boolean`):

When true, only profiles whose category matches an influencer-leaning list (e.g. Blogger, Artist, Public Figure, Digital Creator) pass the filter — helps automatically exclude most brands and organizations. Default false. Requires profile data.

## `categoryFilter` (type: `string`):

Business-category filter — restrict to profiles whose Instagram-set category matches this exact value (e.g. «Public Figure», «Beauty, Cosmetic & Personal Care», «Fitness Trainer»). «any» (default) = no filter. Independent of filterForInfluencers. MCP clients see only the first ~10 enum entries due to truncation; programmatic callers can pass any of the 1090+ exact values.

## `filterCombination` (type: `string`):

How to combine the Business Category filter and the Keywords filter when BOTH are set.
**AND** (default): a profile must match both filters (existing behavior).
**OR**: a profile passes if it matches **either** the category **or** any of the keywords. The output dataset adds a **Matched By** column showing which filter(s) let each profile through (`category`, `keyword`, or `both`).

## `mustBeVerified` (type: `boolean`):

When true, only verified (blue checkmark) accounts pass the filter. Default false. Useful for vetted-creator outreach lists; off by default since most creator audiences are unverified.

## `clearSavedData` (type: `boolean`):

Check this to clear all saved data from previous runs and start clean. Uncheck to resume a previous run from where it left off. Turning on Offline Mode keeps your saved profiles regardless of this setting.

## `enableOfflineMode` (type: `boolean`):

Re-filter the profiles a previous run already retrieved, without fetching anything new. Makes no API calls and charges nothing. Every profile a normal run retrieves is saved - including the ones your filters rejected - so if a run came back empty, turn this on, loosen a filter, and run again for free.

## `excludeAccounts` (type: `array`):

Usernames to skip BEFORE they are analyzed or charged — used for cross-run deduplication. Discovered candidates matching this list are dropped before any per-profile cost is incurred. Accepts bare handles, @handles, or profile URLs (one per line); matched case-insensitively. Leave empty for no exclusion.

## Actor input object example

```json
{
  "postUrls": [],
  "engagementType": "both",
  "maxCountEngagers": 0,
  "extractEmail": true,
  "extractPhoneNumber": false,
  "extractWebsiteUrl": true,
  "extractBusinessCategory": true,
  "extractPhysicalAddress": false,
  "analyzeQuality": true,
  "minEngagementRate": 0,
  "extractPosts": false,
  "searchContactsInPosts": false,
  "keywords": [],
  "keywordLocation": "bio_or_name",
  "locationKeywords": "",
  "profileLanguage": "any",
  "minFollowers": 0,
  "maxFollowers": 0,
  "lastPostDays": 0,
  "lastReelDays": 0,
  "minPostsInPeriod": 0,
  "postsCheckPeriodDays": "30",
  "minMedianViews": 0,
  "viewFollowerRatioMin": 0,
  "viewFollowerRatioMax": 0,
  "contactInfoType": "any",
  "hasWebsite": false,
  "accountType": "any",
  "filterForInfluencers": false,
  "categoryFilter": "any",
  "filterCombination": "AND",
  "mustBeVerified": false,
  "clearSavedData": true,
  "enableOfflineMode": false,
  "excludeAccounts": []
}
```

# Actor output Schema

## `liveStatus` (type: `string`):

No description

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

No description

## `runSummary` (type: `string`):

No description

## `engagementCoverage` (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 = {};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,afanasenko/instagram-post-likers-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/O4zQah1DrkNNLBcx7/builds/08H2ftxQjwlkP1wfJ/openapi.json
