TikTok Search API — Find Videos by Keyword

Search TikTok videos

Videos matching a search term, in TikTok’s own ranking for it. There is no sort or date filter, because TikTok offers neither publicly. Paged: about 12 videos a page, 1 credit per page fetched.

POST /search-videos Search & hashtags
https://api.fetchlayer.dev/tiktok/search-videos
OpenAPI JSON

Headers

Authorization string required

Bearer token — your FetchLayer API key.

Parameters

query string required

What to search TikTok for. Search takes no sort or date filter: sending either is a 400 naming the field, not an option that is quietly ignored.

limit integer optional

Maximum videos returned, up to 500. Use -1 for the maximum. TikTok serves about 12 search results a page, so a larger limit 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.

pages integer optional

Pages of results to return in one request, up to 20. Use -1 for the maximum. Each page fetched is 1 credit, and the response reports how many as pagesFetched. 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. When leaving out videos already returned would bring a response short of limit, it takes one more page to fill it; that page shows in pagesFetched, so a request can cost one credit more than the pages asked for. (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 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

search-videos

Search TikTok videos

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.

query string

The term that was searched.

videos object[]

The matching videos, in TikTok’s ranking. For some health topics TikTok shows a support notice with the results, which still come back, with a note such as "TikTok shows a support notice with these results ("You are more than your weight"); the results are listed as TikTok shows them." For a few terms TikTok shows no results at all: the response is an immediate 200 with videos empty, hasMore false and a note such as "TikTok does not show search results for "suicide"; it shows a support resource instead (988 Suicide and Crisis Lifeline). Asking again will not change this." Repeating the search will not change it.

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.

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.

Request

curl -X POST "https://api.fetchlayer.dev/tiktok/search-videos" \
  -H "Authorization: Bearer ss-your-key" \
  -H "Content-Type: application/json" \
  -d '{"query":"space launch","limit":12}'

Response

{
  "transport": "tiktok",
  "notes": [],
  "pagesFetched": 1,
  "query": "space launch",
  "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": 12,
  "hasMore": true,
  "nextCursor": "tt1.eyJyIjoic3YiLCJrIjoic3BhY2UgbGF1bmNoIiwiYyI6IjAiLCJuIjoxMn0"
}
Product overviewTikTok API: what it returns, pricing and use cases