TikTok Video API — Fetch One Video’s Data by URL or ID

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.

POST /video-detail Videos
https://api.fetchlayer.dev/tiktok/video-detail
OpenAPI JSON

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"
  }
}
Product overviewTikTok API: what it returns, pricing and use cases