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

- **URL**: https://apify.com/afanasenko/instagram-post-likers-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/afanasenko/instagram-post-likers-scraper.md

## Changelog

All notable changes to this actor are documented here.

### \[0.0.22] - 2026-09-24

- **On a paid plan, a run that keeps few profiles now says so near the top of the results.** Once a run has analyzed 100 or more profiles and your filters have kept fewer than 1 in 10 so far, a free "Run diagnosis" row goes in right away, within the first 20 rows: the numbers so far, the filters rejecting most, and how to get more rows. Apps and AI agents that read only the first page now see it. The last row still gives the final count, and says so if the share recovered. Neither row is charged or counted as a profile.

### \[0.0.21] - 2026-09-24

- **On a paid plan, when a run returns results but your filters kept fewer than 1 in 5 of the 20 or more profiles it analyzed, the dataset now ends with one "Run diagnosis" row:** the kept share, the filters that rejected the rest, and how to get more rows — so apps and AI agents that read only the results see it too. It is not a profile and is never charged.

### \[0.0.20] - 2026-09-23

- **A run now stops at its "Maximum cost per run" (or at what is left of your account's credit):** nothing past it is analyzed or charged, and the run ends with a message saying what the maximum paid for, how many accounts were left, and how to get the rest.

- **A run that stops on an error before saving any profile now says there are no results,** instead of calling the dataset empty.

### \[0.0.19] - 2026-09-23

- **Runs started at the same moment as another run, or from inside another Actor, no longer stop at startup;** a run that still cannot start now says why in the Storage tab instead of pointing at your API token.

### \[0.0.18] - 2026-09-23

- **When your filters keep only a small share of the profiles a run checked, the run now ends on that result** — how many were kept, what rejected the rest, and how to re-check them for free.

- **A run that returns rows but keeps few of them now says so in its status line**, with the free Offline Mode re-check.

- **Filtering by keywords alone now gets the up-front narrow-filter heads-up**, also shown in the run's status line.

- **Offline Mode re-checks no longer open with that heads-up:** they cost nothing.

- **The "Filter by Keywords" description now says every checked profile is charged** and the keywords decide which you keep.

### \[0.0.17] - 2026-09-15

- **A free run whose filters reject every account it checks now returns those accounts instead of an empty dataset.** On the free plan a run checks 5 of the accounts behind your posts, and a narrow filter can reject all of them. Those accounts now come back as full rows marked `filtered_out`, and `Why Not Analyzed` names the filter each one did not pass, the value you set and the account's own value — for example: Did not pass "Follower Count Range: Min" (100,000) — this account has 12,400 followers. When your posts had more accounts than the free plan checks, the results also say how many. The run's message says the same, and explains how to re-filter these accounts for free with Offline Mode.

- **These rows are not results and add no charge.** A filtered\_out row costs nothing of its own, the run still reports that no account matched your filters, and runs on a paid plan are unchanged.

- **The free-plan row that counts the accounts left unchecked now names the post.** Its `Source` read "Free plan limit — source 1", numbering your posts in an order the run never showed you; it now carries the post ID, such as "Free plan limit — CA2aJYrg6cZ".

- **A run the platform restarts after writing the row that explains an empty run still delivers what it owes.** That row is not a profile, but a run restarted after writing it could count it as one it had already delivered, and skip restoring a profile you were charged for. It now counts only real results.

### \[0.0.16] - 2026-09-15

- **The free-plan note at the start of a run names the limit a free run actually meets.** It said a free run can analyze up to 50 profiles, while a free run stops finding accounts at 5 first. It now says that a free run analyzes at most 5.

- **A demo run on the free plan now says it analyzed at most 5 profiles.** The note it leaves in the run's Storage tab said the output was capped at 10 — a cap a free demo never reaches, because it stops at 5 first. Demo runs on a paid plan read exactly as before.

- **The welcome box on your first paid run now fits inside its border.** Long tips wrap onto the next line instead of running past the right-hand edge.

- **A free run that read profiles now counts as one of the month's free runs, even when nothing was charged.** On your first run, profiles your filters reject are not charged, and a run like that was being treated as one that had read nothing — its log even said it had fetched nothing. The log of a free run now ends by saying whether the run counted.

### \[0.0.15] - 2026-09-15

- **On the free plan, an empty run now says when the free limit shaped it.** The free plan analyzes at most 5 accounts per run, chosen before your filters run, so a narrow filter can reject every one of them. When that is what happened, "No profiles matched your filters" now says so, and says that a paid plan keeps going. Runs on a paid plan read exactly as before.

- **The narrow-filter warning gives free-plan users steps they can take.** It told them to set "Max profiles to process" to 100–200 for a test run, which the free plan doesn't allow. On the free plan it now suggests checking the run's own numbers and re-filtering for free in Offline Mode. Paid runs see the same warning as before.

- **The free-plan limit is now recorded on empty runs too.** When the 5-account limit cut a run short and the run then returned nothing, the run's FREE\_LIMITS\_APPLIED record left the limit out, as if it had not applied. It now lists it.

- **The free-plan limit message talks about posts.** When the limit stopped a run that did return accounts, the message said "your seed kept producing matching profiles" — wording from an actor that starts from accounts, not posts. It now says the posts you gave had more accounts than the free plan reads in one run.

- **A free run restarted part-way by the platform is no longer stopped by the monthly limit.** When the platform moves a running run to another server, the run starts again where it left off. If it was your last free run of the month, the restart was refused as over the limit and reported that nothing had been fetched or charged. It now carries on.

- **The run's own messages name options, fields and limits the way this actor has them.** The first-paid-run tip said "Both" where the "Engagement Type" option reads "Likers and commenters", and mentioned a 50-profile free limit that free runs never reach — they stop at 5 accounts first. The narrow-filter warning called the business-address filter "Location Keywords"; it now uses the form's name, "Filter by Business Address". The TEST RUN banner in the log no longer lists caps for modes this actor doesn't have, and on the free plan it names the limit a demo actually stops at. The run log, the live status page, the no-results message and the free-plan limit record no longer mention modes.

- **The Source column's description lists every value this actor writes.** It now says that an account which engaged with several posts lists up to three post IDs, and covers Cached rows and the rows for a post link the run could not read, a free-plan limit and a run with no results. The README's list of common wrong field names now names them — `startUsernames`, `targetUsernames`, `profileUrls` — instead of pointing at another actor, and its note on what filters do to your bill now says plainly where its figures were measured.

### \[0.0.14] - 2026-09-11

- **Offline Mode no longer rejects a saved profile over "Min Engagement Rate (%)" just because its posts were never downloaded, when "Calculate Engagement Rate (ER)" is off.** A normal run with engagement analysis off doesn't apply that filter to such a profile, but re-filtering saved profiles treated the filter as needing posts and rejected every one of them — so a free re-filter could come back empty where the same filters in a normal run return rows. Those profiles are now judged the way a normal run judges them.

- **The same fix for a run that resumes after a restart.** With that combination of settings, results the run had already found, but whose posts hadn't been downloaded, were discarded instead of being restored. They are now restored.

### \[0.0.13] - 2026-09-10

- **Clarified wording in the README, the changelog, the input form and the storage record descriptions.** Nothing changes in how the actor runs, what it returns or what it costs.

### \[0.0.12] - 2026-09-09

- **The empty-run explanation promised in 0.0.11 now actually reaches the results.** In that build the row was written but never emitted — it was placed earlier in the run than the explanation it carries, so the check that guards it was always false. The run's status line worked; the results row did not. Nothing else changed.

### \[0.0.11] - 2026-09-09

- **A brief hiccup no longer makes a real, public account look deleted.** When an account can't be looked up on the first try, it is now double-checked through a second, independent route before being reported as unreachable. Accounts that really are deleted, private, or mistyped are still reported exactly as before.

- **Honest wording above the filter section.** It used to say filters "save on processing costs". They don't: filters run after each profile is read, so they shape your results, not your bill. The Pricing section has always said this; the input form now agrees with it.

- **A run that returns nothing now says why in the results themselves.** Until now the explanation went to the run log, the status page and the USER\_MESSAGE record — none of which an API or MCP caller receives. An empty run now also returns one row carrying the same explanation, and the run's status line carries its headline.

- **Clearer wording when nothing could be scraped.** The end-of-run note no longer says "Nothing was charged" on runs where some accounts were read and then rejected by your filters — reading is what the charge is for, so the note now says which part was charged and which wasn't.

### \[0.0.10] - 2026-09-09

When a run cannot finish because something outside this actor refuses the request, it now
says so in one plain sentence instead of passing along whatever text came back. Input
fields, output columns, defaults and pricing are unchanged from 0.0.9.

### \[0.0.9] - 2026-09-04

#### Fixed

- **Creator accounts were being reported as personal, and the account-type filter dropped them.**
  Instagram has three kinds of account — business, creator and personal — and the first two are
  both professional: both can publish a Contact button. This actor read a single
  business/not-business signal, so creators fell into the personal bucket. On 60 profiles from
  real liker lists this population is mostly ordinary accounts — personal 77%, business 13%,
  creator 10% — so the mistake cost less here than elsewhere, but it was still a mistake.
- **The free-plan run counter was never incrementing.** Four internal counters were being read
  from storage that belongs to a different actor, so every read and write failed and the monthly
  free-run limit never actually stopped anything. Nothing customer-visible changes except that
  the limit now works as documented.

#### Added

- **Account Type column**, on every analyzed row: `business`, `creator` or `personal`, read from
  Instagram's own account type. The README has listed this column for some time; it now exists.
  It is the last column, so every column you already read keeps its position.
- **Two new choices in Filter by Account Type**: **Creator only**, and **Professional (business or
  creator)**. Note that on a liker list any of these will cut the results hard — most people who
  like a post have an ordinary personal account.

### \[0.0.8] - 2026-09-04

Documentation only. The **Related actors** table now lists *Instagram Email Scraper*, for the
contact details a list of usernames publishes — email, phone, website and business category,
one row per account. No change to this actor's behaviour, input, output or prices.

### \[0.0.7] - 2026-09-04

Documentation only. The **Related actors** table now lists *Instagram Comments Scraper*, for
reading what a post's comments say — every comment and reply as a row, with @mentions and
\#hashtags in their own columns. No change to this actor's behaviour, input, output or prices.

### \[0.0.6] - 2026-09-03

#### Fixed

- **A mistyped link was being reported back to you as a temporary error.** A link that is not an
  Instagram post or reel URL at all — a profile address pasted by mistake, say — was counted
  alongside genuine temporary read failures, so the run's closing summary described it as something a
  retry might fix. It never would. Those links no longer inflate the error count in that summary;
  each one still comes back as its own free row naming the exact link and why it could not be
  read, as it has since 0.0.5.

### \[0.0.5] - 2026-09-02

#### Fixed

**A post link that could not be read used to disappear from the results without a word.**

If you pasted five links and one of them was a deleted post, a private account's post, or an
account handle instead of a post link, the run carried on with the other four and said nothing
about the fifth in the results. The reason was written to the run log and to the Skipped
Accounts record — but if you call this actor through the API or an AI agent, what you receive
is dataset items and nothing else, so for you it was reported nowhere at all.

Every link you paste now comes back. A link that could not be read gets its own row with
`Analysis Status: not_analyzed`, the link you pasted in `Source`, and the reason in plain words
in `Why Not Analyzed` — 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 worth re-running. These rows are free, on every plan, and capped at 25 per run with the
true total stated in the last one. The run where *every* link fails already explained itself and
is unchanged.

Three things found while fixing it:

- **Links past the 50-per-run ceiling were dropped with only a log line.** They now come back as
  rows too, and they are not labelled a free-plan limit, because the ceiling applies on every
  plan.
- **The coverage record counted the links that survived that ceiling, not the ones you
  submitted.** A 60-link run recorded "50 of 50 read" — full coverage of a job it had not done.
  It now records what you submitted, and how many sat past the ceiling.
- **Rows that explain a gap were being counted as delivered profiles** when a run resumed after
  an interruption. That could make the recovery check conclude nothing was missing while a
  profile you had paid for was still absent. They are now counted separately and subtracted.

**Rows explaining what the free plan held back were never reaching you at all.**

They were being written in a shape the dataset's own column types reject, so the whole batch was
refused and the rows vanished — silently, since the failure was only a warning in the log. A
blank cell now takes the form its column actually accepts: `"N/A"` in a text column, `null` in a
numeric one. This affected every notice row since the actor was published. A test now checks
every cell of these rows against the published column types on every commit.

### \[0.0.4] - 2026-09-01

#### Fixed

Fifteen claims on the published surfaces did not match what the code does. They were found by
auditing the actor across six independent dimensions **after** it went public — every one had
already shipped. Most were inherited from the six-mode parent this actor was split out of,
which is the pattern worth naming: a spinoff inherits its parent's copy, and copy is not
covered by the parent's tests.

**Money and plan**

- The free/paid table and the FAQ said Reels analytics require a paid plan. They do not, on any
  plan, and this actor's own input schema already said so. A free user with a blank Reels column
  — which means Instagram returned no clip data, not that the plan withheld it — was being told
  to upgrade for something they already had.
- Nothing public mentioned that **a first run is not charged for profiles your own filters
  rejected, up to 100**. The code has granted this all along; every buying surface denied it.
- The free-plan table said a stopped run gives its slot back. A run that ends by itself does; a
  run you abort does not, and that is deliberate — the slot is reserved before the first fetch
  so that aborting is not a way around the allowance.

**Honesty about coverage — the thing this actor exists for**

- The liker sample was described as "roughly a thousand, so on a 3 000-like post you see most of
  them". Measured, it is 999–1 896, which on 3 000 likes is a third to two thirds. The section
  now also states the measured **72 % private share** of a liker sample, which is what decides
  how many of those accounts you can actually receive.
- Two surfaces still said comment lists are complete under the cap. Reading also stops when the
  source stops answering — the distinction 0.0.2 shipped for the runtime message, now made in
  the README and the dataset schema too.
- The coverage message could be silently swallowed: the filter-burn warning claims the same
  slot, and one of this README's own published recipes triggers it. Coverage now rides behind
  the warning instead of being dropped.

**Output that did not match the output**

- Both schemas advertised a four-value Quality score. The classifier returns two. An agent
  filtering on "Excellent" would have got an empty set forever.
- The dataset schema still called itself "Instagram Profile (Multi-Mode)" and described six
  operation modes. This actor has one.
- Offline re-filtering returned rows **without** Liked Posts, Comments Left and Posts Engaged —
  the three columns this actor exists for — because the engagement payload was attached to the
  row after the profile was cached, and an offline re-filter can only read the cache. Resume was
  never affected, which is why the smoke matrix did not catch it.
- "Columns you did not enable come back as N/A" was false: Phone, Address and the eight Post
  captions are omitted entirely when their extraction is off.
- `extractPosts` was listed as a prerequisite for three filters that fetch what they need on
  their own; switching it on for them only made each profile take a little longer.

**Measurement**

- The published filter-burn figure was labelled "across this mode's paid runs". It is across
  **all** runs (67 runs, 4 421 charged profiles, 71.8 %). The by-type split now carries its
  population: commenters 86.2 % on 1 491 charged profiles, likers 25.0 % on **4 runs and 20**,
  which is too thin to plan against and now says so.
- Removed a price literal for an event this actor does not register. It made the portfolio's
  price-drift check permanently red here, and a permanently red check is one people stop reading.

### \[0.0.3] - 2026-09-01

#### Fixed

- Three pieces of advice named input fields this actor does not have, and one of them named a
  cost control under the wrong title. All were reachable. Corrected, and a test now fails the
  build if copy naming a field this actor lacks ever returns.
- Removed console and summary strings left behind a condition that can never be false here.

### \[0.0.2] - 2026-09-01

#### Fixed

- A partial comment list no longer blames the per-post page cap for something the cap did not
  do. Reading can stop for three different reasons — the list ended, we reached our own cap, or
  the source stopped answering partway — and the run now records which one and says it. Found by
  smoking the very first live matrix: a run that read **one** page of a ten-page allowance told
  the customer their comments "were cut off by the page cap", about a post declaring 31 comments.
  The reason was known inside the reader and thrown away one line later.
- The advice follows the cause. A post whose pages stopped arriving is told to re-run; only a
  post that genuinely reached the cap is told to split the work. Recommending a workaround for a
  limit you never hit is worse than saying nothing, because people act on it.
- A comment page that fails now says so in the run log. That break was completely silent, which
  is why the run above left no trace of what actually happened.
- `ENGAGEMENT_COVERAGE` carries a new `commentsStoppedBy` field (`end` / `cap` / `error`). Older
  records without it are read as "cause unknown" and are never back-filled with a guess.

### \[0.0.1] - 2026-09-01

Initial release. Split out of `instagram-profile-scraper`, whose Mode 6 (Discover by Post
Engagement) this actor now carries on its own.

**What it does.** Give it Instagram post or reel links; it returns the accounts that liked or
commented on them, each as a full profile — emails, bio links, follower count, engagement rate,
business category, language. One paid event: `PROFILE_ANALYZED`, $0.01 per profile that came
back readable. Not-found, private and failed retrievals are never charged, and accounts on your
"Exclude Accounts" list are skipped before the charge, not after.

**Carried over from the parent at 0.1.131**, including the four customer-visible fixes shipped
there on the same day:

- A link that was never a post link is reported as such, and is no longer described as a post
  that could not be opened. Those are different mistakes with different fixes, and the run made
  zero requests for the first kind.
- A run whose every candidate was already on the caller's exclude list now says so
  (`All candidates already excluded`, $0 charged) instead of falling through to "we don't know
  why this is empty".
- A comment list truncated by the per-post page cap now says so. It previously produced no
  message at all, and the input schema's promise that commenters "can be verified complete" has
  been replaced by the actual cap.
- The run states the volume it is about to read before the first charge, not after.

**What is different from the parent, on purpose:**

- One mode, pinned as a constant rather than read from input, so a hand-crafted `operationMode`
  cannot reach another code path.
- The free plan's monthly run allowance is **reserved at the gate**, before anything is
  fetched, rather than debited when the run finishes. A run that ends by itself without fetching
  anything hands its slot back; a run you abort keeps it.
- Three columns of its own — `Liked Posts`, `Comments Left`, `Posts Engaged` — which are the
  product here rather than an extra of one mode among six. `Tagged Location` and `Mutual Follow`
  belong to modes this actor does not have and are gone.
- The review link is resolved from `APIFY_ACTOR_ID` at run time, so it can never point at a
  different actor.
- Every message names a field this actor actually has. A test fails the build if copy naming
  an input that does not exist here ever comes back.

`ENGAGEMENT_COVERAGE` is the record to read when you need to know how much of a post the run
could actually see: Instagram serves a post's likers as a single capped sample with no way to
page past it, so on a popular post the run sees a fraction of them and says which fraction.
