All notable changes to this actor are documented here.
[0.3] - 2026-08-30
Root cause (found after verifying 0.2 live — see below)
Live verification of the 0.2 fix (apify call with includeCreatorProfile: true, and a raw
POST /v2/acts/.../runs call with the same input) turned up a second, independent, and
arguably more severe bug: every run's authorMeta/likes/comments/views/etc. were
silently omitted, regardless of what the caller requested.
The run log showed [videos] N/3 videos formatted — the code path that only runs when
include_creator_profile is False — even though the input explicitly set
includeCreatorProfile: true. Reading back the actual materialized INPUT key-value-store
record for that run (GET /v2/key-value-stores/{id}/records/INPUT) showed why:
The Apify platform had injected includeDetailedVideoData: false into the input on its own —
confirmed live across three separate trigger paths (apify call, a raw REST POST .../runs
call, and a run with no creator-profile key of any kind supplied at all): any input-schema
field with a default is materialized into the actual stored input for every run, Console,
CLI, and API alike, whether or not the caller mentioned that field. The previous code's
comment explicitly assumed the opposite — that includeDetailedVideoData would only appear
in input_data when a caller deliberately set it — and used
if 'includeDetailedVideoData' in input_data as its precedence check. Because that key is
always present (thanks to its schema default), this check was unconditionally true on every
run, and include_creator_profile was unconditionally forced to bool(False) — silently
disabling this actor's entire headline feature ("With Author Profile") no matter what a caller
passed, including an explicit includeCreatorProfile: true. (includeCreatorProfile itself
was safe from this because its schema entry uses prefill, a Console-UI-only hint that is
not materialized into the input record — also confirmed live, by starting a run with no
creator-profile key at all and reading back its INPUT record.)
Fix
src/main.py, main(): rewrote the include-creator-profile precedence logic to no longer
trust presence-in-input_data for a key whose schema default makes it always present.
include_creator_profile now defaults from includeCreatorProfile (falling back to this
actor's own intended True default when the caller didn't set it), and either legacy key
(includeDetailedVideoData, includeVideoDetails) can only turn it on — since both
default to False, a False value from either can never be distinguished from
platform-injected noise and must never override a real True.
Verified live
Re-pushed as build 0.3 and re-ran the same apify call input
(includeCreatorProfile: true, countryCode: "US", maxItems: 3) that exposed the bug —
confirmed the run log now shows [details] N/3 videos processed and every row carries a
populated authorMeta object plus real likes/comments/shares/views/engagementRate
(see the run output pasted in the fix report).
[0.2] - 2026-08-30
Root cause
The actor was broken in two independent, compounding ways. Both were verified live
(curl_cffi / aiohttp / Playwright probes against the real TikTok and TikTok-for-Business
endpoints on 2026-08-30) before any code was changed — neither was "TikTok blocked us" in
the unfixable sense; both had working replacements.
1. Discovery mode's endpoint was truly gone, but a working replacement exists.creative_radar_api/v1/popular_trend/list (the endpoint the previous version called, with
signed user-sign/web-id/timestamp headers scraped from a headless-browser visit to
ads.tiktok.com/business/creativecenter/inspiration/popular/pad/en) is never called by
TikTok's own current Creative Center UI at all — that URL now redirects to a completely
restructured ads.tiktok.com/creative/creativeCenter/trends/... app, found by capturing its
network traffic, which fetches trending videos from a different host and path entirely:
https://ads.us.tiktok.com/CreativeOne/Report/CreativeCenterGetTopContentsList, with a
different parameter set (countryCode, orderByMetric, periodDimension,
periodEndTimestamp, contentLabelIDs) and — a genuine simplification — no signed
headers required at all. Three real TikTok-side constraints on this replacement were
confirmed live and are now handled explicitly instead of silently assumed away:
periodEndTimestamp must be UTC midnight at least 3 days in the past; anything more
recent returns HTTP 200 / code 0 with an empty result (a soft-fail, not an error).
Probed 0-7 days back to find this boundary. The actor now computes UTC-midnight-minus-4-
days at runtime (one day of safety margin past the observed 3-day minimum).
Anonymous (logged-out) access only returns non-empty data for countryCode=US. Every
other country tested (JP, VN, TH, ID, GB, CA, AU, FR) returned HTTP 200 / code 0 with an
empty entityInfos list — the page itself prompts "Go to TikTok One to view more
rankings and data", i.e. a real TikTok login gate, not a bug. The actor now says this
explicitly in the run log instead of reporting a bare "0 videos".
The page query parameter is ignored for anonymous access — page 1, 2, and 26 all
return the identical top-4 rows. There is no true pagination available anonymously, so
broader coverage now comes from sweeping TikTok's own content-category tags
(contentLabelIDs, read live from cc_portal_api/api/trendsTcc with a hardcoded
fallback) and deduplicating, not from paging — and the run log says plainly that this
sweep is the maximum reachable coverage.
2. Video-detail parsing was completely broken, in BOTH the plain-HTTP path and the
Playwright fallback that was supposed to catch it. TikTok no longer embeds SIGI_STATE or
__UNIVERSAL_DATA_FOR_REHYDRATION__ anywhere in a video page's HTML — confirmed live by
fetching real video pages and searching the fully-hydrated DOM: neither script tag exists at
any point, even though the page's visible content (title, player) loads and renders
correctly. The actual itemStruct (author, authorStats, stats) now arrives via a background
XHR the page's own JavaScript fires to /api/item/detail/?itemId=... once it mounts, gated
behind session cookies (ttwid, msToken, tt_csrf_token, etc.) that only a real browser
acquires by loading the page first. Because the previous version's Playwright fallback also
only looked for the two now-nonexistent script tags, it silently returned nothing on every
single video, discovery-mode enrichment or direct-video-URL mode alike — this was the
mechanism producing empty/near-empty datasets even on runs where discovery mode did have
matching TikTok-side data.
Fix
src/main.py: replaced extract_dynamic_headers() and fetch_page_with_aiohttp() (the
dead signed-header capture + dead endpoint call) with fetch_creative_center_page(),
fetch_creative_center_content_tags(), parse_creative_center_entity(), and
map_sort_by_to_metric(), targeting the real, current
CreativeCenterGetTopContentsList endpoint. fetch_and_parse_trending_videos() now
sweeps content-category tags to gather unique videos (no real pagination exists to walk),
logs the US-only anonymous-access constraint explicitly when a non-US country is
requested, and falls back to the endpoint's own videoViews/engagementRate figures when
the per-video detail-page fetch doesn't yield them.
src/main.py: rewrote fetch_video_details_playwright() to intercept the page's own
/api/item/detail/ network response and read itemInfo.itemStruct from it directly,
instead of scraping the (now nonexistent) SIGI_STATE/__UNIVERSAL_DATA_FOR_REHYDRATION__
DOM script tags. The JSON shape returned by /api/item/detail/ is identical to the old
itemStruct, so extract_stats_from_item() and every downstream parser needed zero
changes — only the retrieval mechanism was broken. The old DOM-based check is kept as a
defensive secondary fallback in case TikTok reintroduces embedded state for some page
variant.
sortBy's old like/comment/repost options are now mapped to the replacement
endpoint's orderByMetric=2 (engagement rate), the closest available server-side proxy,
since TikTok's replacement endpoint has no separate like/comment/share-count sort
dimension. The actual per-video like/comment/share counts are unaffected and still
returned on every row from the video-detail-page fetch regardless of sort choice.
Removed the now-unused math and time imports.
.actor/input_schema.json and README.md: updated the countryCode, sortBy,
maxItems, and videoUrls descriptions, plus the README's public-data table, usage
steps, and "How does it work?" section, to describe the actor's real current behavior
(US-only anonymous discovery, engagement-rate sort proxy, content-category sweep instead
of pagination) instead of the previous blanket "discovery is deprecated, always use
videoUrls" framing, which was no longer accurate once the real replacement endpoint was
wired up.
Verified live
Discovery mode (countryCode: "US", maxItems: 5): returned 5 unique real trending
videos with genuine titles, creator handles, verified badges, follower counts, and (for
videos where the detail-page fetch also succeeded) real views/likes/comments/shares.
Direct video-URL mode: full authorMeta (handle, nickname, verified, bio, avatar,
follower/following/heart/video counts), views, likes, comments, shares, bookmarks, and
engagement rate returned correctly via the new /api/item/detail/ interception path.
Note: one specific video URL that had been fetched roughly a dozen times during this same
investigation session started returning no data on later attempts while a fresh,
unhammered video URL succeeded twice in a row immediately afterward — consistent with
TikTok's own per-item/per-session throttling from repeated probing, not a defect in the
fix. Production runs against a given video will not see this pattern.
[0.1] - baseline
Initial version, calling the (unbeknownst at the time) already-retired
creative_radar_api/v1/popular_trend/list discovery endpoint and parsing video pages for
SIGI_STATE/__UNIVERSAL_DATA_FOR_REHYDRATION__, both of which TikTok had already removed
from its current infrastructure.