List videos under a hashtag
Videos TikTok lists under a hashtag, most popular first, plus the hashtag itself with its exact view and video counts. Paged: up to 30 videos a page; 1 credit per page of up to 30 posts delivered, minimum 1 credit per request, stated as credits. pollState (or since, knownIds, lastPollAt and within) returns only the posts new to you, and requireHashtag only those carrying the tag.
/hashtag-videos Search & hashtags https://api.fetchlayer.dev/tiktok/hashtag-videos Headers
Authorization string required Bearer token — your FetchLayer API key.
Parameters
hashtag string optional A hashtag name with or without the leading #, or a hashtag URL. Pass exactly one of hashtag or hashtagId; both or neither is a 400. A hashtag that does not exist is a 404, which takes about 5–10 seconds.
hashtagId string optional The hashtag’s numeric id, as a string (hashtag.id in a response). An alternative to hashtag; an id that names no public hashtag is a 404.
requireHashtag boolean optional true returns only posts that carry the hashtag in their own caption, leaving out (and counting, as incremental.excludedOffTopicCount) the related posts TikTok also lists under it — often half or more, so a poll returns fewer posts. (default: false)
includeSearch boolean optional true also searches for the hashtag and adds posts that search finds and the hashtag’s list does not, each marked source: "search" (the list’s are "list"), after the list’s posts. Adds about 10 seconds. The list and the search are different selections, and search leans towards recent posts: in one check each search for #minecraft found about 20 posts from the last day that 8 requests for its list had not shown, while for a smaller hashtag such as #bookbinding it found none the list did not already have. The same filters apply to search posts (knownIds or pollState, within, since, requireHashtag), limit counts both together, and incremental.bySource gives the counts per source. Use timeoutMs of 45000 or more: with less than about 12 seconds left after the list, the search is skipped and a note says so. It is not run when continuing with a cursor. (default: false)
limit integer optional Maximum videos returned, up to 500. Use -1 for the maximum. TikTok serves a hashtag’s videos at most 30 to a page, so a limit above 30 needs more pages. (default: 30)
include string[] optional Pass ["createdRegion"] to fill each video’s createdRegion in the same request, which is otherwise null on this route. It adds a few seconds. Without timeoutMs the filling stops about 20 seconds after the request starts; with timeoutMs the response always comes within your limit. A video not filled in time keeps null, and a note gives the count — asking again fills the rest. Pass ["exactStats"] (alone or with createdRegion) to fill every video on the page with exactStats, isAdsNative, brandedContentType and accountRegion, as on video-detail; a page of 30 videos adds a few seconds, at no extra cost. With both createdRegion and exactStats, use timeoutMs of 30000 or more. complete and missing say which videos were left without a field asked for, and why.
since integer | string optional Return only videos posted at or after this time: unix seconds or an ISO 8601 date-time such as "2026-10-14T12:00:00Z". Every post published before it is left out. A millisecond timestamp, a time before 2016 or more than a day ahead is a 400 naming the field.
knownIds string[] optional Ids of videos you already have, up to 2000, as numeric strings; they are left out of the response. An empty list means you have none yet, so older posts come back as newly appeared (newlyPublished false) rather than null. Or, instead of pollState, keep the ids yourself: to poll a hashtag for new posts, send knownIds, lastPollAt and within (such as "7d") with limit: -1 and pages: 7, which covers the whole list; that takes about 22–31 seconds, and timeoutMs of 35000 or more lets nearly every request finish. More than 2000, or an id that is not numeric, is a 400 naming the field.
pollState string optional "new" on your first poll, then the nextPollState from your previous response: we remember what you already have for this hashtag, so you need not send knownIds. The simplest way to poll: {"hashtag": "minecraft", "pollState": "<nextPollState>", "within": "2d", "limit": -1, "pages": 7}. One pollState per hashtag; one from a different hashtag is a 400. It is kept for 30 days after its last use, holding the latest 5,000 posts delivered. If a response is lost, send the same request again (the same pollState and the same other fields) within about 10 minutes: you get exactly the same response again, including the same nextPollState. After that, or if you change any field, it is a fresh poll, which may differ, since TikTok’s list changes between requests. A pollState we no longer have returns every listed post as new, with a note and a fresh nextPollState. You can still add knownIds: both are left out. lastPollAt defaults to your previous poll’s time. Any value other than "new" or one we returned is a 400 naming pollState.
lastPollAt integer | string optional When you last asked for this hashtag, in the same forms as since. It leaves nothing out; it sets each returned video’s newlyPublished. A millisecond timestamp, a time before 2016 or more than a day ahead is a 400 naming the field.
within integer | string optional How far back a post may be published and still count as new to you: "12h", "7d", "4w" (hours, days, weeks) or a number of seconds, from 1 hour to 365 days. With knownIds, posts you do not have that were published before the window are left out and counted, so you get only posts new to you and published within it; without it, every older post you have not seen comes back with newlyPublished false. Without knownIds it works like since set that far back. A value outside those forms or that range is a 400 naming the field.
pages integer optional Pages of results to return in one request, up to 20. Use -1 for the maximum. A limit larger than one page needs more pages: with pages: 1 the response holds one page, a nextCursor and a note saying the pages limit was reached. This route bills by posts delivered, not pages fetched: 1 credit per page of up to 30 posts delivered, minimum 1 credit per request. (default: 1)
cursor string optional Continue from a previous response’s nextCursor, passed back unchanged with the same request. A cursor continues only the route, and the account and sort, hashtag, search term or video, it came from; a cursor from anywhere else, or one this service did not return, is a 400. Bare numeric cursors from before still work, except on search-videos, where one is a 400 asking you to repeat the search.
format string optional json or markdown, lowercase; any other value is a 400. Over MCP, markdown returns the result rendered as text; the REST API accepts it and returns JSON. (default: json)
timeoutMs integer optional Time limit for the whole request, in milliseconds (1000–120000); a value outside that range is a 400. When it runs out, a paged request returns what it has with nextCursor, billed for the posts it returned; a request with nothing to return yet fails with a 503 you can retry, marked with the response header X-Timeout-Source: caller. Left out, the request runs as long as it needs.
Response Fields
hashtag-videos
List videos under a hashtag
transport string The data source: always the platform id, "tiktok".
notes string[] Caveats you can act on — a private account, comments turned off, a list that ran out early, paging that has gone as deep as it goes. Empty when there are none. An empty result always comes with a note explaining it, never as an unexplained empty array.
pagesFetched number Pages of results fetched, search pages included. It is not what this route bills: that is 1 credit per page of up to 30 posts delivered, stated as credits.
hashtag.id string | null TikTok’s numeric hashtag id.
hashtag.name string The tag, without the #.
hashtag.url string Canonical hashtag URL.
hashtag.description string | null The tag’s description, or null.
hashtag.coverUrl string | null Cover image, or null.
hashtag.viewCount number | null Total views across TikTok for the tag, exact, or null when it publishes none.
hashtag.videoCount number | null Videos TikTok reports under the tag, exact, or null.
hashtag.roundedStats string[] Which hashtag counts are rounded. Normally empty: both counts are exact.
videos object[] The videos TikTok lists under it. What TikTok lists for a hashtag: popular posts first, not newest first — a post published a minute ago can sit behind years-old ones, and there is no date order. Only roughly the top 170 to 200 posts, then TikTok says there are no more, even for hashtags with millions of posts. A topic feed: between a quarter and nine tenths of the listed posts carry the tag in their own caption (each video’s hashtags says which). Two requests a minute apart share about 70–95% of their posts, in a somewhat different order; most of the rest are older posts TikTok rotates in and out of its selection. Empty for a hashtag with no public videos, with the note "This hashtag has no public videos." TikTok ends the list for a few very broad hashtags after a handful of videos (#fyp lists 1 to 3); the response then has hasMore false and a note giving both numbers.
videos[].newlyPublished boolean | null Present only when since, knownIds, lastPollAt, within, requireHashtag, pollState or includeSearch is set. true: published at or after lastPollAt (or since). false: older (inside within, if it was sent) and not in knownIds, so newly appeared — mostly TikTok rotating its selection rather than a new post. null: cannot be told — no lastPollAt or since, no date from TikTok, or an older post when knownIds was not sent.
videos[].source string Present only with includeSearch: list for a post from the hashtag’s own list, search for one a search for the hashtag found that the list did not include.
incremental object Present only when since, knownIds, lastPollAt, within, requireHashtag, pollState or includeSearch is set: scannedCount (posts checked in the hashtag’s list, returned or not), excludedCount (left out because they were in knownIds, posted before since or the within window, or did not carry the hashtag with requireHashtag), excludedOlderCount (of those, the ones posted before since or the window), olderFloorAt (where the within window started, ISO 8601; null without within), newlyPublishedCount and newlyAppearedCount. videoCount then counts only the posts returned. limit counts posts returned, not those left out, and pages still bounds how far down the list a request goes. These options narrow what TikTok lists; they cannot find a new post TikTok does not list. When nothing new is listed, videos is empty with a note, and that is the answer, not a failure.
incremental.scannedCount integer Posts checked in the hashtag’s list, returned or not.
incremental.excludedCount integer Left out because they were in knownIds, posted before since or the within window, or did not carry the hashtag with requireHashtag.
incremental.bySource object Present only with includeSearch: the posts checked and returned from each source, as list and search.
incremental.bySource.list.scannedCount integer Posts checked from the hashtag’s list.
incremental.bySource.list.returnedCount integer Of those, posts returned.
incremental.bySource.search.scannedCount integer Posts checked from the search for the hashtag.
incremental.bySource.search.returnedCount integer Of those, posts returned.
incremental.excludedOffTopicCount integer Of excludedCount, the posts left out by requireHashtag for not carrying the hashtag in their own caption.
incremental.excludedOlderCount integer Of excludedCount, those posted before since or the within window.
incremental.olderFloorAt string | null Where the within window started, as an ISO 8601 date-time; posts published before it were left out. null without within.
incremental.newlyPublishedCount integer Returned posts with newlyPublished true.
incremental.newlyAppearedCount integer Returned posts with newlyPublished false.
videos[].id string Numeric video id, which video-detail and comments both accept.
videos[].url string Canonical link to the video. This is what you use to open or embed it.
videos[].description string | null The caption, or null when there is none.
videos[].createdAt string | null ISO 8601 timestamp of when it was posted, or null.
videos[].durationSeconds number | null Length in seconds, or null.
videos[].isPhotoPost boolean True for a photo carousel rather than a clip.
videos[].coverUrl string | null The cover frame. An image, so it re-fetches through GET /media normally.
videos[].assets object[] An ordered list, so a clip and a carousel are handled the same way: a clip yields one asset of kind "video", a carousel one of kind "image" per frame. Each carries url, width, height and downloadUrl. A video asset’s url is TikTok’s own link, which expires within hours and does not open on its own; use downloadUrl. Every post has a video or images, so an empty list means its media could not be read this time: on a request with include, the video then lists assets in missingFields and the page has complete false with a missing entry {field: "assets", reason: "failed", count}.
videos[].assets[].downloadUrl string | null A https://api.fetchlayer.dev/tiktok/media link that keeps working after TikTok’s own link expires. On a video asset it is ?video=<id> and delivers the video as an MP4 without the TikTok watermark; it is null when the creator has turned off downloads. On an image asset it is ?url=… and delivers the image. Each request to it costs 1 credit.
videos[].stats object playCount, diggCount, commentCount, shareCount, collectCount and repostCount. A null counter means TikTok did not publish that figure; a real zero comes back as 0. Views, likes, comments and shares are rounded once they reach 10,000 — roundedStats says which. Counts are as TikTok shows them to US viewers. TikTok keeps a copy of the counts for each region, and the copies differ by a few units, so successive requests can differ slightly.
videos[].roundedStats string[] Which counters in stats TikTok publishes only rounded: to the nearest 100 from 10,000, the nearest 100,000 from 1M, the nearest 100M from 1B. Empty means every counter is exact. Saves (collectCount) are always exact.
videos[].author object id, uniqueId, nickname, avatarUrl, signature, verified, privateAccount, profileUrl and secUid.
videos[].music object | null id, title, authorName, durationSeconds, original, coverUrl, playUrl and url, or null.
videos[].hashtags string[] Without the #, as TikTok tags them on the post rather than parsed out of the caption.
videos[].mentions string[] Usernames without the @.
videos[].mentionedUsers object[] Every account tagged in the caption, in caption order: username, userId (an exact string), secUid, text, start, end and unresolved, each null when TikTok published no value — so username and secUid can be null.
videos[].mentionedUsers[].unresolved string | null null when the tag leads to an account. "account_unavailable" when TikTok names the account’s userId but no longer publishes its username or secUid — the account was deleted, banned or is otherwise unavailable; userId, text, start and end still come back, for example {"username": null, "userId": "6794638798661157893", "secUid": null, "text": "@Weight Watchers", "start": 17, "end": 33, "unresolved": "account_unavailable"}. "not_published" when TikTok published no account id for the tag.
videos[].mentionedUsers[].text string | null The tag as the caption shows it, including the @. Often the display name rather than the username: "@E.L.F Cosmetics" tags eyeslipsfacecosmetics. Use username, userId or secUid to identify the account.
videos[].mentionedUsers[].start number | null Where the tag starts in description; end is where it stops, exclusive. Both count UTF-16 code units, so description.slice(start, end) equals text in JavaScript; in Python, slice the UTF-16 encoding.
videos[].hashtagDetails object[] Every hashtag in the caption, with id (or null), name without the #, text with it, and start and end counted like mentionedUsers.
videos[].isAd boolean | null TikTok’s own isAd value: true when TikTok runs or has run the post as a promoted ad, null when TikTok sends no value. Not the creator’s own #ad disclosure. The three flags combine freely: a post can be an ad and a paid partnership, or a partnership that is also shoppable.
videos[].isPaidPartnership boolean True when the post carries TikTok’s paid-partnership label, which is exactly when commercialSignals.adLabelVersion is 2. False when it is unmarked.
videos[].isShoppable boolean True for a shop or product video: commercialSignals.isECVideo is above 0, or the post carries TikTok’s e-commerce link type 33. False when it is unmarked.
videos[].commercialSignals object TikTok’s own values behind isPaidPartnership and isShoppable, exactly as TikTok publishes them, for callers who want the numbers. null means TikTok sent no value for the post. TikTok does not publish an ad-type or branded-content code beyond these.
videos[].commercialSignals.adLabelVersion number | null TikTok’s ad-label value, or null. 2 is its paid-partnership label, and isPaidPartnership is true exactly when this is 2.
videos[].commercialSignals.brandOrganicType number | null TikTok’s own branded/organic code, such as 1001, or null. TikTok does not explain what the codes mean, and 1001 appears on ads, partnership posts and some unmarked posts alike, so treat it as informational, not as a flag.
videos[].commercialSignals.isECVideo number | null Above 0 (seen: 1) when TikTok marks the post as a shop or product video; null when TikTok sends no value.
videos[].commercialSignals.adAuthorization boolean | null True when the creator allowed the post to be used in ads, or null. Not a sign that it is running as an ad.
videos[].commercialSignals.anchorTypes number[] TikTok’s type codes for the links attached to the post: 35 a product link (always with isECVideo 1), 54 an editing-app link, 28 an effect link. Other codes are passed through as TikTok sends them. Empty when no links are attached.
videos[].createdRegion string | null The two-letter region TikTok records this post as created in, such as "US". Filled on video-detail; on user-videos, search-videos and hashtag-videos it is null unless the request has include: ["createdRegion"]. Not the account’s region.
videos[].language string | null The language TikTok assigned to the caption, or null.
videos[].exactStats object | null Only with include ["exactStats"]: the exact counts — playCount, diggCount, commentCount, shareCount and collectCount — as whole numbers, never rounded, where stats rounds from 10,000 up. null, with a note, when they could not be filled. Exact counts are filled for nearly all posts — about 19 in 20 resolve, most on the first ask. The rest come back null with a note; asking again later can fill them. A miss is a normal 200, not an error: the video comes back in full with its usual stats (six counts, including repostCount) and roundedStats, and exactStats, isAdsNative, brandedContentType and accountRegion are present and null, with a note — so check exactStats === null rather than the HTTP status. exactStats has five counts; it has no repostCount. On a list, a page can be partly filled: a note gives how many videos are null, and video-detail can fill those one at a time. The notes read, verbatim: “The exact counts could not be found for this video, so exactStats and accountRegion are null; ask again shortly.”; when your timeoutMs ran out first, “The exact counts could not be filled in time, so exactStats and accountRegion are null; ask again, or allow more time.”; on a list, “The exact counts could not be filled for N of these M videos in time, so exactStats is null for them; ask again or use video-detail for those videos.”; and when only the region is missing, “The exact counts were filled, but this account’s registered region did not come with them, so accountRegion is null.”
videos[].exactStats.playCount integer Exact views.
videos[].exactStats.diggCount integer Exact likes.
videos[].exactStats.commentCount integer Exact comments.
videos[].exactStats.shareCount integer Exact shares.
videos[].exactStats.collectCount integer Exact saves.
videos[].isAdsNative boolean | null Only with include ["exactStats"]: TikTok’s own ad marker for the post. true is rare; false and null are what nearly every post carries.
videos[].brandedContentType integer | null Only with include ["exactStats"]: TikTok’s numeric branded-content type — 0 none, 1 paid partnership, 7 commission; other codes come through as TikTok sends them (4 has been seen). null when not filled.
videos[].accountRegion string | null Only with include ["exactStats"]: the two-letter region TikTok records the account as registered in, such as "US" — the account’s own country. Not where this post was made (createdRegion), and not where the account’s recent videos were posted (postingRegion on user-profile); the three can differ. null, with a note, when no region came with the exact counts, so exactStats can be filled while this is null.
videos[].missingFields string[] Only on a request with include: the fields asked for (exactStats, accountRegion, createdRegion) that this video was left without, and assets when its media could not be read (its assets list is then empty); empty when it has them all. A field TikTok does not publish for the post, such as a post with no recorded created-in region, is not listed.
complete boolean Only on a request with include: true when every video has every field asked for. A 200 with complete false still carries every video: count it as a delivered response with fields to ask for again, not as a failure.
missing object[] Only on a request with include: one entry per field and reason, as {field, count, reason}, counting the videos left without it; empty when complete.
missing[].field string exactStats, accountRegion, createdRegion or assets. assets counts the videos whose media could not be read, so their assets list is empty; ask again for them. exactStats missing means isAdsNative, brandedContentType and accountRegion are null too; accountRegion on its own means the exact counts filled and only the account’s region did not come with them.
missing[].count integer How many videos were left without that field, for that reason.
missing[].reason string time_limit: the time limit ran out before it was filled (your timeoutMs, or the default when you set none); ask again, or allow more time. not_found: it was looked up and not found this time; ask again later. failed: the lookup failed; ask again. unavailable: not offered on this request. New reasons may be added, so treat an unknown one as “not filled”. createdRegion is only ever time_limit or failed; accountRegion is only ever not_found; assets is only ever failed; exactStats can carry any of the four.
videoCount number How many videos are in this response, list and search posts alike. credits is what the request is billed for them.
hasMore boolean Whether more results exist beyond what was returned.
nextCursor string | null An opaque token (tt1.…) to pass back unchanged as cursor, with the same request, to continue exactly where this response stopped. Continuing never repeats or skips a video: with sort latest on user-videos, every video comes back once, newest first. With sort popular, on search and on hashtags the same holds at each boundary, but TikTok can re-rank between requests, so keep the ids you have if every video exactly once across a long walk matters. null when there is nothing further.
nextPollState string Present only when the request sent pollState: send it as pollState on your next poll of this hashtag.
stopReason string Why the list stopped. With a nextCursor to continue: limit (as many posts as limit asked for), pages (the pages limit was reached), time (your timeoutMs would not fit another page), interrupted (TikTok did not return the next page). Without one: end (the end of what TikTok lists for this hashtag, at most about 170–200 posts). cut_short: TikTok ended the list early, well short of what it reports for the hashtag. Asking again often returns more, and when this response got any posts it carries a nextCursor that continues from where it stopped, with hasMore true; a continuation that gets nothing ends without one. When a list is cut short, follow its nextCursor once. If that continuation comes back with no posts and cut_short (and no cursor), do not repeat it: TikTok stopped listing the hashtag there for this request. Wait a little and poll the hashtag again from the start, with your latest pollState if you use one; the list changes between requests, and a later poll usually goes deeper. A request whose list is cut short has already been retried up to twice before it answers.
credits integer What this request is billed: 1 credit per page of up to 30 posts delivered; minimum 1 credit per request (a poll delivering nothing new costs the base request charge). The whole list of a hashtag (about 170 posts) is about 6; a poll that delivers a few new posts is 1–2; a poll with nothing new is 1. Every request is billed at least 1, including a repeated request and a continuation with a cursor.
Request
curl -X POST "https://api.fetchlayer.dev/tiktok/hashtag-videos" \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"hashtag":"nasa","limit":30}'Response
{
"transport": "tiktok",
"notes": [],
"pagesFetched": 1,
"hashtag": {
"id": "3301",
"name": "nasa",
"url": "https://www.tiktok.com/tag/nasa",
"description": null,
"coverUrl": null,
"viewCount": 57143791248,
"videoCount": 1952955,
"roundedStats": []
},
"videos": [
{
"id": "7686148096895569165",
"url": "https://www.tiktok.com/@nasa/video/7686148096895569165",
"description": "Turns out you can dance in space! 🪩🕺💫🛰️ Microgravity just unlocked some new moves.",
"createdAt": "2026-09-16T15:00:41.000Z",
"durationSeconds": 58,
"isPhotoPost": false,
"coverUrl": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/...",
"assets": [
{
"kind": "video",
"url": "https://v16-webapp-prime.us.tiktok.com/video/tos/useast5/...",
"width": 720,
"height": 1280,
"downloadUrl": "https://api.fetchlayer.dev/tiktok/media?video=7686148096895569165"
}
],
"stats": {
"playCount": 27600,
"diggCount": 4367,
"commentCount": 173,
"shareCount": 104,
"collectCount": 273,
"repostCount": 0
},
"roundedStats": [
"playCount"
],
"author": {
"id": "7664638705177150477",
"uniqueId": "nasa",
"nickname": "NASA",
"verified": true,
"privateAccount": false,
"profileUrl": "https://www.tiktok.com/@nasa"
},
"music": {
"id": "7686148224364694286",
"title": "original sound - NASA",
"authorName": "NASA",
"durationSeconds": 58,
"original": false,
"url": "https://www.tiktok.com/music/x-7686148224364694286"
},
"hashtags": [],
"mentions": [],
"mentionedUsers": [],
"hashtagDetails": [],
"isAd": false,
"language": "en",
"isPaidPartnership": false,
"isShoppable": false,
"commercialSignals": {
"adLabelVersion": null,
"brandOrganicType": null,
"isECVideo": null,
"adAuthorization": null,
"anchorTypes": []
},
"createdRegion": null
}
],
"videoCount": 30,
"hasMore": true,
"nextCursor": "tt1.eyJyIjoiaHYiLCJrIjoibmFzYSIsImMiOiIzMCJ9",
"stopReason": "limit",
"credits": 1
}