# Changelog of TikTok Profile Scraper — Emails, Followers & Commenters (`afanasenko/tiktok-profile-scraper`) Actor

- **URL**: https://apify.com/afanasenko/tiktok-profile-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/afanasenko/tiktok-profile-scraper.md

## Changelog

All notable changes to the TikTok Profile Scraper actor are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

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

#### Fixed

- Starting a second run while another is still working no longer deletes the saved profiles the
  first run is writing, so a restarted run re-reads them instead of analyzing them again.
- If Apify does not give a run access to your saved profiles, the run keeps its own copy and
  completes, instead of stopping at start or stalling before its results are saved.

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

#### Fixed

- **A run now stops at its "Maximum cost per run".** Profiles are only looked up while the limit
  can pay for them. Everything charged is kept, and the run's message says how to get the rest.

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

#### Changed

- **Several target accounts now share your profile limit instead of the first one taking all of
  it.** In Followers / Following and in Engaged Audience, a run with more than one target account
  and a profile limit used to fill the whole limit from the first target and never read the
  others. The limit is now split between your targets, so every account you name contributes rows
  and the `Source` column shows each of them. When a target has fewer accounts to give than its
  share, the rest goes to your other targets, so a run still returns as many rows as before — but
  the same input can now return a different set of rows.
- **With both Scrape Followers and Scrape Following switched on**, each target's share is now split
  between its two lists as well. A limited run could previously fill up on followers and never
  read the following list you asked for.
- **Target accounts that share an audience no longer come back short.** When your targets have
  followers or commenters in common, the run keeps reading until it reaches your limit, instead of
  stopping early with more accounts still available.

#### Added

- **A message when your limit is too small to reach every target.** If your profile limit is lower
  than the number of target accounts — for example 2 profiles across 3 targets — the targets it
  could not reach are named in `USER_MESSAGE` (`targets_skipped_limit_v1`), together with the
  setting to raise, and counted in `RUN_SUMMARY` as `targetsSkipped`. Nothing is charged for a
  target that was not read.

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

#### Changed

- Clarified wording in the documentation and the changelog. Nothing changes in how the actor runs,
  what it returns or what it costs.
- When a run fails for a reason on our side, the error message now stays short and plain.

### \[0.0.181] - 2026-09-07

#### Fixed

- **Creator accounts were being reported as personal, and the account-type filter dropped them.**
  TikTok has three kinds of account — business, creator and personal — and the first two are both
  professional. This actor recognised only business, so every creator fell into the personal
  bucket. Measured on 139 accounts read from real runs: personal 77%, creator 11%, business 11% —
  so **Business only** was discarding half of the professional accounts it found, and **Personal
  only** was returning creators mixed in with ordinary accounts.

#### Added

- **Account Type column**, on every row: `business`, `creator` or `personal`, read from TikTok's
  own account type. A business account is the one that publishes a company name, and often an
  address or phone; a creator account is a public figure without them. The column is last, so
  every column you already read keeps its position.
- **Two new choices in Filter by Account Type**: **Creator only**, and **Professional (business or
  creator)** for when you want either kind of professional account. Running an influencer
  campaign? Choose **Creator** — the old advice to choose **Personal** was wrong.

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

#### Fixed

- **The `ER (incl. shares) (%)` column showed nothing in the Output tab.** The value was there
  the whole time — every download and every API read of the dataset has carried it — but the
  results table was told to render that column in a way it could not, so the cell came out
  blank on screen. The same applied to `Median Views`, `Most Viewed Video Plays` and
  `Avg. Shares (Recent)` on profiles with no posts, where the column says `No Content`. All
  four now display. Datasets keep the display settings they were created with, so this shows
  from your next run; results you already have are unaffected and were never missing the data.

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

#### Fixed

- **Follower and following lists came back empty when they were not empty.** A read of a
  target's list could come back with nothing in it even though the account has a full list,
  and the actor treated that as final: it concluded the account followed nobody, moved on
  quietly, and finished the run as a success. Lists are now read correctly, and a read that
  cannot be completed is reported instead of being passed off as an empty list.
- **A run that could not read your accounts now says so.** Previously such a run showed "No
  followers or following could be read from your target accounts" and offered a misspelled
  username, a private account or a hidden list as the causes — all of which point at your
  input, and none of which need be true. There is now a separate message
  (`USER_MESSAGE list_unreadable_v1`, stop reason `Target lists temporarily unavailable`)
  that names the affected accounts, says the fault is ours, and asks only for a re-run.

#### Added

- **Far more accounts per target, and much faster.** Follower and following lists are read
  more deeply and complete in a fraction of the time they used to take.
- **Short reads are declared.** When a list is longer than what could be retrieved, the run
  log says `Collected N of M` instead of stopping quietly at N, and `RUN_SUMMARY` carries
  `listsUnreadable` and `listsTruncated`.
- **A message for runs that delivered rows but almost nothing survived the filters**
  (`most_filtered_v1`): it names the filter doing the rejecting and explains that filters
  stop at the first failure, so only the top one is worth relaxing. Previously every
  zero-result explanation was gated on a run saving nothing at all, so a run that saved 1 of
  30 said nothing.
- **`RUN_SUMMARY.filterRejections`** — the per-filter breakdown, which until now existed only
  inside the internal resume checkpoint.

### \[0.0.176] - 2026-08-25

#### Added

- **Monthly free-run limit: 15 runs per calendar month** for free accounts (resets on the 1st,
  UTC). The per-run caps are unchanged — the median free run analyzes one profile and never
  touches them; the monthly limit is what a free account actually consumes. A run started after
  the limit succeeds with $0, fetches nothing, and explains itself (`USER_MESSAGE
  free_monthly_limit_v1`, `RUN_SUMMARY.blocked` with the reset date, terminal status message).
  A failed run that charged nothing returns its slot. Demo/test runs never count.
- **Withheld free-plan preview rows.** When the free caps trim your input (a username list
  beyond 100, or more than 3 target accounts), the trimmed usernames — your own input — are now
  published at the end of the dataset as uncharged preview rows: `Row Status =
  'withheld_free_plan'`, every metric column empty (nothing was fetched), and a `Why Withheld`
  sentence naming exactly what a paid plan would analyze. Every delivered row now carries
  `Row Status = 'delivered'`, so previews are filterable. Two new dataset columns appended
  last; both dataset views updated.

### \[0.0.175] - 2026-08-18

#### Fixed

- **A person's name in a username field is no longer split into two unrelated accounts.** A space
  used to separate entries, so `Resee Stevens` was read as two usernames — `Resee` and `Stevens` —
  and both exist on TikTok, belong to other people, and were analyzed and charged for. Entries are
  now separated by commas and line breaks only. An entry containing a space is named as a name,
  skipped, and never fetched. Pasting a comma-separated list into a single row still works exactly
  as before.
- **A TikTok link that is not a profile is no longer fetched as if it were one.** `tiktok.com/video/…`
  and `tiktok.com/tag/…` used to be read as accounts called `video` and `tag`. Written without
  `https://`, such a link was read as an account called `tiktok.com`. Both are now recognised and
  reported instead of being looked up.
- **An entry containing `?` or `#` is no longer quietly trimmed into a different account.** `who?!`
  used to become `who`, which is a real account and not the one you asked for. It is now reported as
  an entry that cannot be a username.
- **Runs happening at the same time no longer share progress with each other.** The record of which
  accounts a run had already handled was kept for the whole actor rather than per run, so two runs
  going at once could read each other's — one could skip accounts you had paid for, or wipe the
  other's progress mid-run. Each run now keeps its own.
- **A restarted run no longer forgets its budget or its results.** When a run moved to another server
  it used to start its spending back at zero and could lose up to the last fifty rows it had gathered
  but not yet written. It now saves everything it has collected before the move, and picks up where
  it left off with its spending, its counts and its progress intact. Its final summary describes the
  whole run rather than only the part after the restart.
- **A run that searched and found nobody no longer calls itself completed successfully.** Its stop
  reason now says that no accounts were found, the same way an unreachable list already did.

### \[0.0.174] - 2026-08-17

Maintenance build — no user-facing changes. Input schema, output dataset
columns, KVS records, console output, error messages, defaults, and pricing
are all unchanged from 0.0.173. Saved tasks continue to work identically.

### \[0.0.173] - 2026-08-17

Maintenance build — no user-facing changes. Input schema, output dataset
columns, KVS records, console output, error messages, defaults, and pricing
are all unchanged from 0.0.172. Saved tasks continue to work identically.

### \[0.0.172] - 2026-08-13

#### Fixed

- **A list whose entries are all unusable no longer reports itself as an empty list.** It used to say
  the field "came through empty" — sending you to check a field you had in fact filled in. It now
  names the entries it could not use, explains what a username looks like, and its stop reason says
  the input was the problem.

### \[0.0.171] - 2026-08-13

#### Fixed

- **A username that arrives with a link stuck to it is now read correctly.** If a username reached the
  actor joined onto its own profile address — for example `name` followed immediately by `https:` —
  the run took the whole thing as the username, found no such account, and ended after about a second
  reporting that no accounts could be analyzed. The username is now recovered from it and analyzed
  normally. Sending a plain username or a full profile link on its own worked before and still does.
- **A username that cannot be used is now named.** The closing message used to say only that the
  accounts could not be found, which left you to work out which entry was at fault — and an entry
  with a link stuck to it looks correct at a glance. Unusable entries are now listed by name, with a
  note on what a username may contain.
- **A run that reached none of its accounts no longer calls itself completed successfully.** Its stop
  reason now says what actually happened. The same correction applies to a run whose filters rejected
  every profile.

### \[0.0.170] - 2026-08-10

#### Changed

- The rules that keep internal technical detail out of run output are now identical across all our
  actors. No user-visible change here — this build only brings this actor in line with the shared copy.

### \[0.0.169] - 2026-08-10

#### Fixed

- **A failed run no longer prints internal technical detail.** When a run failed because a request
  was refused or went unanswered, the run log and the run's `RUN_SUMMARY` record carried the raw
  reply. None of it was ever useful to you, and none of it belongs in a run log. A failed run now
  says the failure was on our side, and nothing more.

#### Added

- **`region`** — the run's region, previously only settable under an older key. The old key still
  works, so nothing you have saved changes.

### \[0.0.168] - 2026-08-10

#### Changed

- The free-plan limit ids listed in the run's `FREE_LIMITS_APPLIED` storage record now use one
  shared vocabulary across all our actors, so the same limit reads the same wherever you meet it.
  The human-readable message on each entry is unchanged.

### \[0.0.167] - 2026-07-29

#### Changed

- **The Store listing now says what this actor returns** — emails, bio links, follower and like
  counts and video stats on every profile, across follower and following lists, username
  enrichment, and the accounts that commented on a video — and the price line shows the
  per-profile price instead of a generic label.

### \[0.0.166] - 2026-07-28

#### Fixed

- **The record that lets an interrupted run resume is now declared in the actor's storage schema.**
  It was always written, but the schema did not list it, so anything reading the schema had no way
  to know it existed.

#### Changed

- Local helper files that were never needed at run time no longer travel with the actor, so the
  build is smaller.

### \[0.0.165] - 2026-07-28

Maintenance build — no user-facing changes.

### \[0.0.164] - 2026-07-27

#### Added

- **New `Source` column on every row.** It says how each profile entered your results — `Follower of <target>` or `Following of <target>` in Mode 1, `Direct Input` in Mode 2, `Commenter on <target>` in Mode 3. Previously, a run covering several target accounts produced one undifferentiated list and there was no way to tell which account any given profile came from. This column was already being worked out internally on every run; it simply was never written to the output.
- **Mode 3 rows now carry `Comments Left` and `Posts Engaged`.** `Comments Left` counts every comment the account left across the target posts that were read, and `Posts Engaged` counts how many different posts they commented on — which separates someone engaging steadily from someone leaving a burst of replies under a single post. Both describe what the run actually read: scanning stops once your profile limit is reached, so treat them as a floor rather than a lifetime total.

#### Changed

- **Mode 3 results are now ordered by engagement instead of follower count** — most comments first, then most posts engaged, with follower count as the tie-breaker. Follower count is the wrong headline for a mode whose purpose is finding the people who actually engage. Rows are written in batches and ordered within each batch, so sort the exported dataset by `Comments Left` if you need a strict ranking across the entire run.
- **In Mode 3, an account that comments on several of your target accounts is now counted across all of them**, and its `Source` lists each one. Previously it was credited only to whichever target happened to be scanned first, and repeat comments from the same person were discarded rather than counted.

No pricing change: the same single Profile Analyzed event fires once per analyzed profile, exactly as before, and no run performs extra retrievals because of this release.

### \[0.0.163] - 2026-07-06

#### Changed

- Every "Upgrade" link in run messages now says what's behind it: paid plans start at $29/mo with $29 in platform credits, and the link opens your plan page in Apify Console.

### \[0.0.162] - 2026-07-06

#### Fixed

- The "Upgrade" link in run summaries now opens your Apify plan page directly (the old link form could land on the wrong billing tab).

### \[0.0.161] - 2026-07-02

#### Fixed

- **Profiles that fail to retrieve are no longer billed.** Not-found, deleted, and private accounts never counted as deliverable rows, but each retrieval attempt was still charged as a Profile Analyzed event. Billing now fires only after a profile actually comes back readable — matching what the pricing table and FAQ always promised.
- **Trying the actor with the default demo input is now always cheap.** The pre-filled example input (e.g. `willsmith` in Mode 1 with no profile cap) previously started a full-size run — on a paid account that meant an unbounded follower crawl. Runs using the untouched demo input are now detected and capped at 10 profiles (~$0.02), with a storage notice explaining the cap and how to switch to your own accounts.

#### Added

- **Every empty run now explains itself.** A run that ends with 0 rows writes a `USER_MESSAGE` storage record naming the cause and the fix: no candidates found (per-mode guidance with known-good starter accounts), every profile rejected by your filters (with the top rejection reasons and which filter to loosen), or no account reachable (not-found / private / temporary-error breakdown). Previously these runs reported only "Completed successfully".
- **Free-plan caps now say what was held back — with real numbers.** When the free plan trims your run (a list longer than 100 profiles, more than 3 target accounts, or the 100-profiles-per-run ceiling), the run writes a notice with the exact counts (e.g. "Your list has 110 profiles — the free plan analyzed the first 100") plus a `FREE_LIMITS_APPLIED` storage record. Caps you set yourself (like `maxCountList`) are never presented as plan limits.
- Input form: honest guidance where it was missing — Mode 1 states it works on public accounts with visible lists only, the filter section explains that filters run after profiles are fetched, and username fields point to verified starter accounts.

#### Changed

- README: mode-selection guidance in four places (each mode reads its own username field), an expectation line for Mode 1, three new FAQ entries (no-login/no-cookies, scheduled runs, "how do I get more results"), one-click example tasks, and a store description without pricing boilerplate.

### \[0.0.160] - 2026-05-07

#### Changed

- **README** — aligned to Apify quality template. Hero rewritten with value-first prose and a "beyond what TikTok's official Research API offers" comparison; numbered 3-step Quick start; new `💡 Tips & Best Practices` section (4 sub-sections × 3 bullets); new `🛟 Support & feedback` section pointing to Apify Store reviews / bookmark / Issues tab; promoted disclaimer to dedicated `⚖️ Is it legal to scrape TikTok?` H2 with link to Apify's web-scraping legality blog; FAQ expanded to 12 Q\&As (including legality, ban risk, data freshness, cost cap); 3-mode descriptions condensed to a table. No functional changes — same input, output, dataset shape, and pricing.

### \[0.0.159] - 2026-05-05

#### Fixed

- Run-level analytics no longer fail with "Insufficient permissions for the key-value store" on every paid run — the per-user session and first-run timestamp stores were using actor-agnostic names that, under Apify's LIMITED\_PERMISSIONS scoping, were owned by another actor in the same account. Renamed to actor-scoped names so this actor's runs can read/write its own analytics state.

#### Changed

- Improved diagnostics when every account in the input was already analyzed by an earlier run. Previously, the actor silently skipped every cached account, completed with **0 saved profiles**, and reported "Completed successfully" — paying users saw a successful run that produced nothing. Runs in this state now show **Stop Reason: All accounts already processed** and write a `USER_MESSAGE` storage record telling you to set "Clear saved data before run" to true if you want to re-analyze them.
- Per-account "skipped because already analyzed in a prior run" is now counted and surfaced in the run-complete summary block.

### \[0.0.4] - 2026-05-04

#### Changed

- README expanded with a full output sample, pricing math examples, three filter recipes (nano-influencer outreach / English-speaking creators / B2B partnership shortlist via engaged commenters), an FAQ section, and a comparison table to other actors in this portfolio for multi-platform users.
- Every output column now has a per-field description, type, and example available in the Apify Console's dataset view and to clients that read the actor schema programmatically — covers all 16 base columns plus the conditional Email, Website, and Post N columns.
- Input field help text rewritten throughout — each field now leads with what it does and any prerequisites (e.g. "Required when keywordLocation is 'posts' or 'anywhere'"), so it's clearer what to set before running.
- SEO title / description tightened on the Apify Store page (no functional change).

### \[0.0.3] - 2026-04-30

#### Fixed

- Followup to 0.0.2: extended the bounded timeout to the remaining mid-run storage calls — the totals-counter write, the per-batch checkpoint save, and the per-account checkpoint and detailed-cache writes. Under reduced-permission contexts these were each stalling silently and preventing the run from progressing past the main analysis loop, even with the start-of-run paths already bounded. Storage operations now degrade to no-ops when unavailable rather than blocking the run.

### \[0.0.2] - 2026-04-30

#### Fixed

- Followup to 0.0.1: extended the 3-second-timeout bounding to the run-startup paths too. Under reduced-permission contexts, the saved-data clear (`setValue` / `dataset.drop` / `openDataset`) and checkpoint resume (`dataset.getData` / `getValue`) were also stalling silently, preventing the run from ever reaching the main loop. With these calls bounded, runs proceed even when the underlying storage operations are unavailable.

### \[0.0.1] - 2026-04-30

#### Fixed

- Bound every async finalization step (run-summary write, run-metadata fetch, first-run timestamp lookup, analytics POST) with a 3-second timeout each, plus a 60-second cleanup safety net. Under reduced-permission run contexts these calls were stalling silently and the run could reach the platform timeout instead of exiting cleanly. Completed runs now terminate within ~15 seconds of the "Run complete" log.
