Get one video in full
One TikTok video in full by id or URL, in the same shape every route returns videos in, plus createdRegion, the region it was posted from. include ["exactStats"] adds the exact counts behind the rounded ones. A short share link is a 400: open it, or pass it to resolve-url, first.
/video-detail Videos https://api.fetchlayer.dev/tiktok/video-detail Headers
Authorization string required Bearer token — your FetchLayer API key.
Parameters
video string required A video URL or a numeric video id. Shortened share links are rejected — resolve them first.
include string[] optional Pass ["exactStats"] to also get the exact counts behind the rounded ones — views, likes, comments, shares and saves as whole numbers in exactStats (a video showing playCount 7,900,000 has exactStats.playCount 7,856,927, say) — plus isAdsNative, brandedContentType and accountRegion. It adds about 2 to 5 seconds when the counts are filled, and up to about 25 seconds in the rare case they cannot be; it costs nothing extra, and without it the response is unchanged. 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. Any other value is a 400.
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 pages 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
video-detail
Get one video in full
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, which is what the request costs: one credit per page. resolve-url reports 0 and is not billed.
video object The video, in the shape below.
video.id string Numeric video id, which video-detail and comments both accept.
video.url string Canonical link to the video. This is what you use to open or embed it.
video.description string | null The caption, or null when there is none.
video.createdAt string | null ISO 8601 timestamp of when it was posted, or null.
video.durationSeconds number | null Length in seconds, or null.
video.isPhotoPost boolean True for a photo carousel rather than a clip.
video.coverUrl string | null The cover frame. An image, so it re-fetches through GET /media normally.
video.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}.
video.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.
video.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.
video.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.
video.author object id, uniqueId, nickname, avatarUrl, signature, verified, privateAccount, profileUrl and secUid.
video.music object | null id, title, authorName, durationSeconds, original, coverUrl, playUrl and url, or null.
video.hashtags string[] Without the #, as TikTok tags them on the post rather than parsed out of the caption.
video.mentions string[] Usernames without the @.
video.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.
video.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.
video.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.
video.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.
video.hashtagDetails object[] Every hashtag in the caption, with id (or null), name without the #, text with it, and start and end counted like mentionedUsers.
video.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.
video.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.
video.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.
video.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.
video.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.
video.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.
video.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.
video.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.
video.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.
video.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.
video.language string | null The language TikTok assigned to the caption, or null.
video.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.”
video.exactStats.playCount integer Exact views.
video.exactStats.diggCount integer Exact likes.
video.exactStats.commentCount integer Exact comments.
video.exactStats.shareCount integer Exact shares.
video.exactStats.collectCount integer Exact saves.
video.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.
video.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.
video.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.
video.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.
Request
curl -X POST "https://api.fetchlayer.dev/tiktok/video-detail" \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"video":"https://www.tiktok.com/@nasa/video/7686148096895569165"}'Response
{
"transport": "tiktok",
"notes": [],
"pagesFetched": 1,
"video": {
"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": "US"
}
}