TikTok Hashtag Video Feed & Stats Extractor
Collect a TikTok hashtag's total view/video counts plus the video feed underneath it - captions, author, music, and engagement - with automatic fallback when the feed is blocked.
Input schema
Input fields, types, required flags, descriptions, defaults, and prefills come from the published definition. Prefill examples are distinct from defaults.
| Field | Type | Required | Description |
|---|---|---|---|
hashtags | array | required | Hashtags to collect. Accepts a bare tag, #tag, or a TikTok tag URL. Case-insensitive, duplicates removed. Up to 10. |
mode | string | optional | What to collect. 'stats' returns one aggregate row per hashtag (cheapest). 'feed' returns the public hashtag video feed. 'search' returns one page of TikTok keyword/hashtag search results. 'auto' returns stats plus the feed and adds search results only when the feed is shorter than one page. |
maxVideosPerHashtag | integer | optional | Upper bound on videos per hashtag for feed, search and auto modes (0–300). Use 0 for stats only. TikTok usually returns fewer than requested: the feed is served in pages of about 30 and many tags stop after roughly 100–150 rows. |
Show all input fields
| Field | Type | Required | Description |
|---|---|---|---|
hashtags | array | required | Hashtags to collect. Accepts a bare tag, #tag, or a TikTok tag URL. Case-insensitive, duplicates removed. Up to 10. |
mode | string | optional | What to collect. 'stats' returns one aggregate row per hashtag (cheapest). 'feed' returns the public hashtag video feed. 'search' returns one page of TikTok keyword/hashtag search results. 'auto' returns stats plus the feed and adds search results only when the feed is shorter than one page. |
maxVideosPerHashtag | integer | optional | Upper bound on videos per hashtag for feed, search and auto modes (0–300). Use 0 for stats only. TikTok usually returns fewer than requested: the feed is served in pages of about 30 and many tags stop after roughly 100–150 rows. |
maxHashtags | integer | optional | Process no more than this many hashtags from the list (1–10). Extra hashtags are skipped. |
includeStatsRow | boolean | optional | Emit the aggregate hashtag stats row before video rows in feed, search and auto modes. Stats mode always emits it. |
region | string | optional | Market used for the hashtag feed. Only the United States is verified to return a public feed; other markets are not exposed yet. |
proxyConfiguration | object | optional | Optional. Leave empty to use the built-in US residential connection with automatic fallback. Set your own Apify proxy configuration here to override it. Feed mode uses the most residential bandwidth, which is billed by Apify proxy usage. |
Download the published input schema
View complete JSON
{
"description": "Collect all-time stats for one or more TikTok hashtags and/or the videos underneath them. Stats mode is the cheapest; feed mode returns the public US hashtag feed; search mode returns one page of keyword search; auto combines stats and feed and adds search when the feed is short.",
"properties": {
"hashtags": {
"description": "Hashtags to collect. Accepts a bare tag, #tag, or a TikTok tag URL. Case-insensitive, duplicates removed. Up to 10.",
"editor": "stringList",
"items": {
"type": "string"
},
"maxItems": 10,
"minItems": 1,
"prefill": [
"fitness"
],
"title": "Hashtags",
"type": "array"
},
"includeStatsRow": {
"default": true,
"description": "Emit the aggregate hashtag stats row before video rows in feed, search and auto modes. Stats mode always emits it.",
"title": "Include Stats Row",
"type": "boolean"
},
"maxHashtags": {
"default": 10,
"description": "Process no more than this many hashtags from the list (1–10). Extra hashtags are skipped.",
"maximum": 10,
"minimum": 1,
"title": "Max Hashtags",
"type": "integer"
},
"maxVideosPerHashtag": {
"default": 30,
"description": "Upper bound on videos per hashtag for feed, search and auto modes (0–300). Use 0 for stats only. TikTok usually returns fewer than requested: the feed is served in pages of about 30 and many tags stop after roughly 100–150 rows.",
"maximum": 300,
"minimum": 0,
"prefill": 0,
"title": "Max Videos Per Hashtag",
"type": "integer"
},
"mode": {
"default": "stats",
"description": "What to collect. 'stats' returns one aggregate row per hashtag (cheapest). 'feed' returns the public hashtag video feed. 'search' returns one page of TikTok keyword/hashtag search results. 'auto' returns stats plus the feed and adds search results only when the feed is shorter than one page.",
"editor": "select",
"enum": [
"stats",
"feed",
"search",
"auto"
],
"enumTitles": [
"Stats only — video and view totals",
"Hashtag feed — videos under the tag",
"Search — one page of TikTok results",
"Auto — stats + feed, search when short"
],
"prefill": "stats",
"title": "Mode",
"type": "string"
},
"proxyConfiguration": {
"description": "Optional. Leave empty to use the built-in US residential connection with automatic fallback. Set your own Apify proxy configuration here to override it. Feed mode uses the most residential bandwidth, which is billed by Apify proxy usage.",
"editor": "proxy",
"title": "Proxy Configuration",
"type": "object"
},
"region": {
"default": "US",
"description": "Market used for the hashtag feed. Only the United States is verified to return a public feed; other markets are not exposed yet.",
"editor": "select",
"enum": [
"US"
],
"enumTitles": [
"United States (verified)"
],
"title": "Region",
"type": "string"
}
},
"required": [
"hashtags"
],
"schemaVersion": 1,
"title": "TikTok Hashtag Stats and Video Feed Input",
"type": "object"
}
Output schema
Published result links and storage locations. This schema is not an exhaustive contract for every field in a dataset record.
| Field | Type | Required | Description |
|---|---|---|---|
data | string | optional | Dataset containing hashtag_metadata stats rows and/or post video rows. Stats rows carry all-time totals; post rows carry the video payload plus source, position, region, scrapedAt, and carriesSearchedHashtag. |
Download the published output schema
View complete JSON
{
"actorOutputSchemaVersion": 1,
"description": "Aggregate hashtag stats rows (video/view totals, source, region) and video rows (caption, author, music, engagement, provenance). Use the recordType field to distinguish rows.",
"properties": {
"data": {
"description": "Dataset containing hashtag_metadata stats rows and/or post video rows. Stats rows carry all-time totals; post rows carry the video payload plus source, position, region, scrapedAt, and carriesSearchedHashtag.",
"template": "{{links.apiDefaultDatasetUrl}}/items",
"title": "Hashtag Data",
"type": "string"
}
},
"title": "TikTok Hashtag Stats and Video Feed Output"
}
Dataset schema
Published dataset views and any declared field definitions. A view can select only a subset of the returned record fields.
Download the published dataset schema
View complete JSON
{
"actorSpecification": 1,
"fields": {
"announcementBody": {
"description": "Optional challenge announcement body text",
"title": "Announcement Body",
"type": "string"
},
"announcementTitle": {
"description": "Optional challenge announcement title",
"title": "Announcement Title",
"type": "string"
},
"author": {
"description": "Author snapshot at time of scraping (id, username, displayName, followerCount, etc.)",
"title": "Author",
"type": "object"
},
"authorStats": {
"description": "Follower, following, heart, video and digg counts for the author",
"title": "Author Stats",
"type": "object"
},
"caption": {
"description": "Post caption/description text",
"title": "Caption",
"type": "string"
},
"carriesAnyHashtag": {
"description": "True when the video's caption carries at least one hashtag entity",
"title": "Carries Any Hashtag",
"type": "boolean"
},
"carriesSearchedHashtag": {
"description": "True when the video's own hashtag entities include the searched tag",
"title": "Carries Searched Hashtag",
"type": "boolean"
},
"collectCount": {
"description": "Number of times post was saved/collected",
"title": "Collect Count",
"type": "integer"
},
"commentCount": {
"description": "Number of comments",
"title": "Comment Count",
"type": "integer"
},
"coverLarger": {
"description": "Large cover banner image URL",
"title": "Cover Image (Large)",
"type": "string"
},
"coverMedium": {
"description": "Medium cover banner image URL",
"title": "Cover Image (Medium)",
"type": "string"
},
"coverThumb": {
"description": "Thumbnail cover image URL",
"title": "Cover Image (Thumb)",
"type": "string"
},
"createTime": {
"description": "Unix timestamp when the post was created",
"title": "Create Time",
"type": "integer"
},
"createdAt": {
"description": "ISO 8601 timestamp of post creation (UTC)",
"title": "Created At",
"type": "string"
},
"desc": {
"description": "Optional hashtag description",
"title": "Description",
"type": "string"
},
"duetEnabled": {
"description": "Whether duets are allowed on this post",
"title": "Duet Enabled",
"type": "boolean"
},
"hashtag": {
"description": "The requested hashtag (without #) on stats rows",
"title": "Hashtag",
"type": "string"
},
"hashtagId": {
"description": "TikTok internal challenge ID (string)",
"title": "Hashtag ID",
"type": "string"
},
"hashtagName": {
"description": "Canonical hashtag name as TikTok stores it",
"title": "Hashtag Name",
"type": "string"
},
"hashtags": {
"description": "Hashtags used in the post, each with id and name",
"title": "Hashtags",
"type": "array"
},
"images": {
"description": "Slideshow images for photo posts (postType='slideshow'). Each item has ordinal, width, height, urlList.",
"title": "Images",
"type": "array"
},
"isAd": {
"description": "Whether the post is a paid advertisement",
"title": "Is Ad",
"type": "boolean"
},
"isCommerce": {
"description": "Whether this is a commercial/brand hashtag",
"title": "Is Commerce",
"type": "boolean"
},
"likeCount": {
"description": "Number of likes",
"title": "Like Count",
"type": "integer"
},
"locationCreated": {
"description": "Country code where the post was created",
"title": "Location Created",
"type": "string"
},
"mediaUrlExpiresAt": {
"description": "ISO 8601 UTC timestamp when signed media URLs expire",
"title": "Media URL Expires At",
"type": "string"
},
"mentions": {
"description": "@mentions in the caption with username and offsets",
"title": "Mentions",
"type": "array"
},
"music": {
"description": "Music/sound used in the post (id, title, authorName, duration, playUrl, coverUrl)",
"title": "Music",
"type": "object"
},
"playCount": {
"description": "Number of video plays",
"title": "Play Count",
"type": "integer"
},
"position": {
"description": "1-based position in the collected feed/search list for the hashtag",
"title": "Position",
"type": "integer"
},
"postId": {
"description": "Unique TikTok post/video ID (string)",
"title": "Post ID",
"type": "string"
},
"postType": {
"description": "Either 'video' or 'slideshow'",
"title": "Post Type",
"type": "string"
},
"postUrl": {
"description": "Direct URL to the TikTok post",
"title": "Post URL",
"type": "string"
},
"profileLarger": {
"description": "Large profile/cover image URL for the hashtag",
"title": "Profile Image (Large)",
"type": "string"
},
"profileMedium": {
"description": "Medium profile image URL for the hashtag",
"title": "Profile Image (Medium)",
"type": "string"
},
"profileThumb": {
"description": "Thumbnail profile image URL for the hashtag",
"title": "Profile Image (Thumb)",
"type": "string"
},
"recordType": {
"description": "Discriminator: 'hashtag_metadata' for aggregate stats rows, 'post' for video rows",
"title": "Record Type",
"type": "string"
},
"region": {
"description": "Market used for the run (US is the verified feed market)",
"title": "Region",
"type": "string"
},
"repostCount": {
"description": "Number of reposts (from statsV2)",
"title": "Repost Count",
"type": "integer"
},
"scrapedAt": {
"description": "UTC ISO 8601 timestamp when the record was built",
"title": "Scraped At",
"type": "string"
},
"searchedHashtag": {
"description": "The normalized hashtag input that produced this record (without #)",
"title": "Searched Hashtag",
"type": "string"
},
"shareCount": {
"description": "Number of shares",
"title": "Share Count",
"type": "integer"
},
"shareEnabled": {
"description": "Whether sharing is enabled on this post",
"title": "Share Enabled",
"type": "boolean"
},
"shareMeta": {
"description": "Share metadata object with title and desc fields",
"title": "Share Meta",
"type": "object"
},
"source": {
"description": "'challenge_detail' on stats rows; 'feed' or 'search' on video rows",
"title": "Source",
"type": "string"
},
"stitchEnabled": {
"description": "Whether stitching is allowed on this post",
"title": "Stitch Enabled",
"type": "boolean"
},
"textExtra": {
"description": "Raw TikTok caption entities (hashtags and mentions) when present",
"title": "Text Extra",
"type": "array"
},
"textLanguage": {
"description": "Language code of the caption text",
"title": "Text Language",
"type": "string"
},
"video": {
"description": "Video metadata (width, height, duration, ratio, playUrl, cover)",
"title": "Video",
"type": "object"
},
"videoCount": {
"description": "All-time number of videos using this hashtag, from statsV2 (string to avoid overflow)",
"title": "Video Count",
"type": "string"
},
"viewCount": {
"description": "All-time views across all videos using this hashtag, from statsV2 (string)",
"title": "View Count",
"type": "string"
}
},
"title": "TikTok Hashtag Stats and Video Feed Dataset",
"views": {
"overview": {
"display": {
"component": "table",
"properties": {
"caption": {
"format": "text",
"label": "Caption"
},
"carriesSearchedHashtag": {
"format": "text",
"label": "Has Tag"
},
"commentCount": {
"format": "number",
"label": "Comments"
},
"hashtagName": {
"format": "text",
"label": "Hashtag Name"
},
"likeCount": {
"format": "number",
"label": "Likes"
},
"playCount": {
"format": "number",
"label": "Plays"
},
"position": {
"format": "number",
"label": "Position"
},
"postId": {
"format": "text",
"label": "Post ID"
},
"postUrl": {
"format": "link",
"label": "Post URL"
},
"recordType": {
"format": "text",
"label": "Type"
},
"region": {
"format": "text",
"label": "Region"
},
"scrapedAt": {
"format": "text",
"label": "Scraped At"
},
"searchedHashtag": {
"format": "text",
"label": "Hashtag"
},
"source": {
"format": "text",
"label": "Source"
},
"videoCount": {
"format": "text",
"label": "Video Count"
},
"viewCount": {
"format": "text",
"label": "View Count"
}
}
},
"title": "Overview",
"transformation": {
"fields": [
"recordType",
"searchedHashtag",
"hashtagName",
"videoCount",
"viewCount",
"source",
"postId",
"caption",
"carriesSearchedHashtag",
"position",
"region",
"scrapedAt",
"playCount",
"likeCount",
"commentCount",
"postUrl"
]
}
}
}
}
Key-value schema
Published key-value store definitions, when provided by this build.
This definition is not present in the default published build. See the published README for available documentation.
Published README
Read the full published documentation
TikTok Hashtag Stats and Video Feed Collector
Collect the numbers behind a TikTok hashtag and the videos underneath it. For each tag you get one all-time stats row — total videos, total views, cover images, commerce flag — plus, when you ask for them, video rows with captions, authors, music, engagement counts, and the exact source each video came from. No TikTok login, cookies, or API key is needed.
What this actor does
- Four modes:
stats(cheapest),feed(the public hashtag video feed),search(one page of TikTok keyword/hashtag results), andauto(stats plus feed, with search added when the feed is short) - One aggregate row per hashtag: all-time
videoCountandviewCount, canonical hashtag name and ID, description, commerce flag, cover images, and campaign announcement text when TikTok provides it - Video rows with provenance:
source(feedorsearch), 1-basedposition,region,scrapedAt, andcarriesSearchedHashtagso you can tell a true tag video from a search match - Rich video payload: post ID/URL/caption/language/creation time, play/like/comment/share/collect/repost counts, author profile and stats, music metadata, video dimensions and cover, slideshow images, hashtags, and mentions
- Deduplicated and capped: repeated videos are removed by post ID, and every mode honours
maxVideosPerHashtag - Empty fields are omitted from every record instead of being written as
null
Output per hashtag stats row
Stats rows have recordType: "hashtag_metadata".
recordType— always"hashtag_metadata"searchedHashtag— the requested hashtag, lowercased and without#hashtag— the same requested hashtag, for convenient filteringhashtagId— TikTok's internal challenge IDhashtagName— hashtag name as TikTok stores itdesc— hashtag description, when presentisCommerce— whether TikTok marks the hashtag as commercialvideoCount— all-time video count, as a string because values can be very largeviewCount— all-time view count, as a stringprofileLarger/profileMedium/profileThumb— hashtag image URLscoverLarger/coverMedium/coverThumb— cover images, when presentannouncementTitle/announcementBody— campaign announcement text, when presentshareMeta—{title, desc}share preview, when presentsource— always"challenge_detail"on stats rowsregion— market used for the runscrapedAt— UTC ISO 8601 timestamp
Output per video
Video rows have recordType: "post".
recordType— always"post"searchedHashtag— the hashtag whose run produced this videosource—"feed"for the hashtag feed or"search"for keyword-search resultsposition— 1-based order in the collected list for that hashtagcarriesSearchedHashtag—truewhen the video's own hashtag entities include the searched tagcarriesAnyHashtag—truewhen the caption carries at least one hashtagregion— market used for the runscrapedAt— UTC ISO 8601 timestamppostId,postType(videoorslideshow),postUrlcaption,textLanguage,locationCreatedcreateTime(Unix epoch),createdAt(ISO 8601)stitchEnabled,shareEnabled,duetEnabled,isAdplayCount,likeCount,commentCount,shareCount,collectCount,repostCountauthor—{id, secUid, username, displayName, bio, verified, privateAccount, avatarUrl, followerCount, followingCount, heartCount, videoCount}authorStats— the author's follower, following, heart, video, and digg counts as integersmusic—{id, title, authorName, original, duration, playUrl, coverUrl}video—{width, height, duration, ratio, playUrl, cover}hashtags[]—{name, id, isCommerce}entries from the captionmentions[]—{username, start, end, awemeId}entries from the captiontextExtra— raw caption entities when presentimages[]—{ordinal, width, height, urlList}for slideshow postsmediaUrlExpiresAt— UTC ISO 8601 expiry of the signed media URLs, when detectable
Input
| Field | Type | Default | Description |
|---|---|---|---|
hashtags |
array | – | Hashtags to collect. Accepts fitness, #fitness, or a TikTok tag URL. Required, 1–10 values |
mode |
string | stats |
stats, feed, search, or auto |
maxVideosPerHashtag |
integer | 30 |
Upper bound per hashtag (0–300). Use 0 for stats only. This is a cap, not a promise — TikTok often returns fewer |
maxHashtags |
integer | 10 |
Process only the first N hashtags (1–10) |
includeStatsRow |
boolean | true |
Emit the stats row before video rows in feed, search, and auto. stats mode always emits it |
region |
string | US |
Market used for the feed. Only US is verified |
proxyConfiguration |
object | – | Optional. Leave empty to use the built-in US residential connection with automatic fallback |
Example: cheapest stats check
{
"hashtags": ["fitness"],
"mode": "stats",
"maxVideosPerHashtag": 0
}
Example: collect 60 videos from a campaign hashtag
{
"hashtags": ["summerfashion"],
"mode": "feed",
"maxVideosPerHashtag": 60,
"includeStatsRow": true
}
Example: keyword search fallback
{
"hashtags": ["booktok"],
"mode": "search",
"maxVideosPerHashtag": 40,
"includeStatsRow": false
}
Example: several tags with auto mode
{
"hashtags": ["#fitness", "gymtok", "https://www.tiktok.com/tag/workout"],
"mode": "auto",
"maxVideosPerHashtag": 90,
"maxHashtags": 3
}
Use cases
- Campaign sizing — compare all-time video and view counts across several brand or campaign hashtags in one run
- Content research — sample current top videos under a niche tag and study hooks, captions, and creators
- Creator discovery — rank authors by follower and engagement counts inside a hashtag feed
- Social listening — track how a hashtag's totals change across repeated snapshots
- Agency reporting — export one clean dataset of hashtag totals plus video-level evidence for a client deck
- Search-gap analysis — compare true feed rows against keyword-search rows using
sourceandcarriesSearchedHashtag
Limits and data freshness
- Region: the public video feed is verified for the United States only. Other markets are not exposed until each one is verified.
- Feed size: TikTok serves roughly 30 videos per page and stops early for many tags. A typical run gets about 100–150 videos before the tag's own feed stops; very large tags can stop sooner. The platform's practical ceiling is far below “all videos”.
- Search size: search returns one page of roughly 50–90 results. It is keyword search, not guaranteed tag membership — use
carriesSearchedHashtagto filter. - No date or sort filter: TikTok's public hashtag feed does not expose a reliable date range or sort order, so this actor does not offer one.
- Stats are all-time:
videoCountandviewCountdescribe every video ever posted with the tag, not only the rows in your dataset. - Freshness: every record carries
scrapedAt. Stats and video counts move continuously on TikTok. - Media URLs expire: video, audio, cover, and avatar URLs are signed and time-limited.
mediaUrlExpiresAttells you when they were expected to expire. Download or copy media promptly if you need it; this actor does not rehost files. - Empty fields are omitted: if TikTok does not return a field for a record, the field is absent rather than
null.
FAQ
Do I need a TikTok login or cookies? No. The actor reads public hashtag pages and public search results without any account.
Do I need to configure a proxy?
No. Leave proxyConfiguration empty and the actor picks a working US connection, falling back automatically if the preferred one is unavailable. Feed mode depends on a US residential connection, which is billed by Apify proxy usage. As a guide, feed runs often add roughly $0.05–0.06 per hashtag in residential transfer; stats-only runs are cheaper. Actual cost varies with run time, row count, and current Apify pricing.
Why did I get fewer videos than maxVideosPerHashtag?
TikTok stops serving the public feed after its own internal limit. The actor also stops when TikTok reports no more pages, when no new videos arrive after repeated scrolls, or when the tag's feed is empty. maxVideosPerHashtag is an upper bound, never a guarantee.
Why is the stats row's view count larger than the views in my videos? Stats are all-time aggregates for the whole hashtag. Your dataset holds only the videos this run could collect.
Why do some search videos not carry my hashtag?
TikTok search matches keywords and related content, not exact tag membership. Check carriesSearchedHashtag; false means the video appeared in search but does not list the searched tag as a hashtag entity.
What is the difference between feed and search?
feed is TikTok's hashtag video feed and is the closest thing to "videos posted under this tag". search is a single page of keyword results and may include only loosely related videos. auto uses the feed and only adds search when the feed returns less than one page.
Does auto always add search results?
No. Search is a supplement, not a replacement. It is used only when the feed delivers no rows or fewer than a full page, up to your per-hashtag cap.
Are duplicate videos removed?
Yes. Videos are deduplicated by post ID within each hashtag. The same video can still appear under two different hashtags because each row records its searchedHashtag.
Do old saved tasks still work?
Yes. Inputs using metadata, posts, topPosts, maxPostsPerHashtag, or includeHashtagMetadata are mapped to the new fields automatically.
Will media links still work tomorrow?
TikTok media links are signed and expire. Use mediaUrlExpiresAt as a guide and refresh the dataset when you need current URLs.
Is this affiliated with TikTok? No. This is an independent third-party actor that reads publicly available TikTok pages and is not operated or endorsed by TikTok.
Python quickstart
Use a schema-derived input and the Python standard library to start a run. An Apify account and token are required for execution.
Source build Jaudae6KZydhY2aLp. Documentation is regenerated from the default published build; private drafts are not included.