# TikTok (/docs/tiktok) TikTok profiles, videos, comments, single-comment lookup, search, trending, sounds, transcripts, and on-screen text through one unified API TikTok accounts and their videos, the comment section down to a single named comment, three flavours of search, per-country trending, sound-level discovery, spoken transcripts, and the text burned into the frame. Commerce data lives on the separate [TikTok Shop API](/docs/tiktokshop.md). Base URL: `/v1/tiktok/...` A TikTok video carries three different texts and each has its own endpoint. The **caption** comes back on `post`, the **spoken words** on `post/transcript`, and the **on-screen overlay** on `video/screen-text`. `post` also surfaces the creator's own text stickers at `post.ext.on_screen_texts` without the OCR pass. ## Quickstart ### 1. Fetch a profile ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/profile?handle=charlidamelio" \ -H "x-api-key: YOUR_API_KEY" ``` ### 2. Fetch their content ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/profile/videos?handle=charlidamelio" \ -H "x-api-key: YOUR_API_KEY" ``` ### 3. Read computed fields When an endpoint supports a computed field and the required source inputs are present, the unified response includes that optional field. Depending on the endpoint, optional fields can include `engagement_rate`, `language`, `content_category`, and `estimated_reach`. See [Computed fields](/docs/computed-fields.md) for formulas, clamping rules, and null semantics. Where an endpoint supports them, list rows also carry judged labels (`computed.labels`, `computed.relevance`) by default at no extra credits; see [Labels](/docs/labels.md). ## Accounts | Endpoint | Credits | What it returns | Key parameters | | ------------------------------- | ------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | `GET /v1/tiktok/profile` | 1 | Display name, bio, follower and like counts, verification, the two-letter region, numeric `user_id` | `handle` or `user_id` | | `GET /v1/tiktok/profile/full` | 5 | Profile, recent videos, and computed metrics in one call instead of three | `handle` or `user_id`, `posts` (1-100, default 25), `include` | | `GET /v1/tiktok/profile/videos` | 1 | A page of recent public videos with captions, view/like/comment/share counts | `handle`, `sort_by`, `region`, `max_cursor`, `label`, `judgments` | | `GET /v1/tiktok/profile/region` | 1 | Just the two-letter country code, for routing or de-duplicating by market | `handle` | | `GET /v1/tiktok/user/followers` | 1 | Who follows this account, paged | `handle` or `user_id`, `min_time` | | `GET /v1/tiktok/user/following` | 1 | Who this account follows, paged | `handle` or `user_id`, `min_time` | | `GET /v1/tiktok/user/audience` | 5 | Top follower countries with sampled counts and each country's share | `handle` | | `GET /v1/tiktok/user/live` | 1 | The account's live room: cover, title, start time, status, viewer and entry counts | `handle` | `user/audience` is **geography only**. Age and gender are not publicly available on TikTok, or on any other platform here, so no endpoint returns them. `user/live` can still return a room after the broadcast has ended. Check `is_live`, `status` and the start time yourself rather than treating a response as proof the stream is on. Answers are cached for at most a minute, an account that has never streamed returns `is_live: false` with an empty room, and a handle that does not exist is a 404 at 0 credits. ## Videos and text | Endpoint | Credits | What it returns | Key parameters | | ---------------------------------- | ------- | --------------------------------------------------------------------- | --------------------------------------- | | `GET /v1/tiktok/post` | 1 | One video in full: caption, engagement, author, sound, video metadata | `url`, `download_media`, `region` | | `GET /v1/tiktok/post/transcript` | 10 | The spoken words from the video's caption track | `url`, `use_ai_as_fallback`, `language` | | `GET /v1/tiktok/video/screen-text` | 5 or 10 | Native text stickers plus AI OCR of a video's cover frame (or the whole video with `frames=all`), or of a photo post's slides, 10 a call | `url`, `frames`, `slide_offset` | | `GET /v1/tiktok/collection/videos` | 1 | The public videos inside a collection URL, paged | `url`, `max_cursor` | `download_media=true` on `post` adds direct media URLs. `use_ai_as_fallback=true` on `post/transcript` transcribes the audio when TikTok publishes no caption track, for videos up to 10 minutes (600 seconds). `data.transcript_format` says which form came back: `webvtt` for a caption track or the AI transcript of a longer video, `text` for the AI transcript of a shorter one. A longer video with no captions returns 400 with `error.details.reason: video_too_long` at no charge. A video long enough to need AI transcription whose speech cannot be transcribed in the `language` you set returns a free 404 with `error.details.reason: language_unsupported`. See [error reasons](/docs/errors.md). `video/screen-text` reads a video's cover frame by default, for 5 credits, so text that appears later in the video is missed. Pass `frames=all` to read the whole video, up to 180 seconds, for 10 credits: the response adds `subtitles` (on-screen captions of the speech, kept out of `texts`), a `timeline` of every text block with the second it first appears, and `duration_seconds`. A video over 180 seconds returns 400 at no charge. When the video file cannot be read this time, the cover is read instead, the call costs 5, and `warnings` says so. On a photo post `frames` has no effect; `slide_offset=10` reads slides 11 to 20. ## Comments | Endpoint | Credits | What it returns | Key parameters | | -------------------------------------- | ------- | ------------------------------------------------------------------ | ---------------------------------------------- | | `GET /v1/tiktok/post/comments` | 1 | A page of the comment section: username, text, likes, reply count | `url`, `cursor`, `label`, `judgments` | | `GET /v1/tiktok/video/comment/replies` | 1 | One thread opened up | `comment_id` **and** `url`, both required; optional `label`, `judgments` | | `GET /v1/tiktok/comment` | 2 or 6 | One named comment, or the comments matching an author or a snippet | `comment_url`, or `post_url` plus `comment_id` | Pull a page of the section first, then expand any thread you want using that comment's id together with the same video URL. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/post/comments?url=https://www.tiktok.com/@stoolpresidente/video/7623818255903329566" \ -H "x-api-key: YOUR_API_KEY" curl "https://www.socialcrawl.dev/v1/tiktok/video/comment/replies?url=https://www.tiktok.com/@stoolpresidente/video/7623818255903329566&comment_id=7623828115408274207" \ -H "x-api-key: YOUR_API_KEY" ``` For the whole nested thread in one call, use `/v1/prism/comments`. See [Which endpoint should I use?](/docs/choosing-endpoints.md) for the full comparison. ### Finding one comment without paging the section TikTok exposes no fetch-one-comment read, so `GET /v1/tiktok/comment` is a bounded server-side scan that stops the instant it finds your target. That is why it is priced flat rather than per page. You can address the target three ways: - **`comment_url`**: a `.../@{handle}/video/{id}?comment_id={cid}` link, an `m.tiktok.com` share link, or a `vm.tiktok.com` shortlink, all resolved for you. TikTok's share sheet emits the `cid` base64-encoded (`?cid=NzY1...`), and that form is decoded automatically. - **`post_url` plus `comment_id`**: the same thing in two parts. `comment_id` must be the numeric id. If the target is a _reply_, also pass `parent_comment_id`: a reply URL does not carry its parent, and supplying it resolves the reply directly instead of scanning. - **A search**: `post_url` plus either `author_username` or `text_contains` (an exact, case-insensitive snippet), returning up to `max` matches (1-20, default 5). ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/comment?comment_url=https://www.tiktok.com/@stoolpresidente/video/7623818255903329566?comment_id=7623828115408274207" \ -H "x-api-key: YOUR_API_KEY" ``` Three things make it safe to call speculatively. It costs a flat **2 credits**, or **6** with `deep_scan=true`, which raises the page ceiling and the deadline for deeply-buried comments; there is nothing in between and nothing above. A not-found returns `404` and is fully refunded, so you can point it at a comment that may have been deleted without paying to find out. And every response carries `lookup.position_hint`: store it, pass it back on the next lookup of the same comment, and the scan probes that last-known location first. For recurring tracking of a known set, `POST /v1/prism/comment-lookup` re-checks up to 25 comments in one call with per-item found, not-found, errored, or deferred status. ## Search | Endpoint | Credits | What it returns | Key parameters | | ----------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | `GET /v1/tiktok/search` | 1 | Video-only keyword search, paged and filterable | `query`, `date_posted`, `sort_by`, `region`, `label`, `relevance`, `judgments` | | `GET /v1/tiktok/search/top` | 1 | TikTok's own Top-tab relevance ranking rather than a recency list | `query`, `publish_time`, `sort_by`, `region` | | `GET /v1/tiktok/search/users` | 1 to 31 | Accounts matching a term, with follower counts and verification; `include=profile` adds the bio, region, bio link and category per row | `query`, `cursor`, `include`, `limit` (1-30) | | `GET /v1/tiktok/search/hashtag` | 1 | The feed under one tag, the right shape for tracking a campaign | `hashtag`, `region`, `cursor`, `label`, `judgments` | | `GET /v1/tiktok/search/suggestions` | 1 | Autocomplete terms for a partial query, one fixed list | `query`, `region` | `search/top` returns videos only; it does not include accounts. Expand a seed with `search/suggestions` first, then call `search` or `search/top`. ### Bio and region next to user search results `search/users` rows carry the counts, the identity and the verification flag, and nothing the search surface does not publish: `author.bio`, `author.location`, `author.ext.bio_link` and `author.ext.business_category` are null on a plain call. Add `include=profile` and every row is filled from the profile lookup in the same call: the bio, the two-letter region, the link in the bio and the account category. 1 credit for the page plus 1 per row filled from a fresh lookup, so 31 at most for the 30-row page; rows already in cache are free, rows that could not be filled are refunded and named in `_warnings`, and the counts the row already carried are never overwritten. `limit=N` (1 to 30) takes the top N rows and caps the extra credits with it, so `limit=5&include=profile` costs at most 6. The join adds 4 to 9 seconds on a fresh page (never more than 12; the lookups run in parallel and the call waits for the slowest) and nothing when the rows are already cached. The `hydration` block on the response lists the rows looked up, filled and served from cache, the credits held and kept, and the milliseconds the join added. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/search/users?query=cooking&include=profile" \ -H "x-api-key: YOUR_API_KEY" ``` ### Follower counts next to search results `search` and `search/top` rows carry the creator's follower count on `post.ext.author_followers`; the hashtag feed does not. Every row carries two join keys: `post.author.username` and `post.ext.author_id`, the creator's numeric user id. Either one resolves to the current follower count on `author.followers` through `GET /v1/tiktok/profile`: pass the username as `handle` or the id as `user_id`. The id survives username changes; the handle is the one that works on audience-gated accounts, so retry a `404` id lookup as a handle lookup before treating the creator as gone. For a crawl, deduplicate creators across the whole run first. The same creators recur across keywords, and you pay once per creator rather than once per row. `POST /v1/prism/profiles` resolves up to 50 usernames in one call at 1 credit per resolved profile, refunds rows that fail, and never serves from cache. Match rows to your input by each row's `index`. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/profile?user_id=7527113447973487646" \ -H "x-api-key: YOUR_API_KEY" ``` ## Trending and sounds | Endpoint | Credits | What it returns | Key parameters | | ---------------------------- | ------- | -------------------------------------------------------------------- | ------------------ | | `GET /v1/tiktok/trending` | 5 | Popular videos now; `feed=local` keeps most in one country | `region`, `feed` | | `GET /v1/tiktok/song` | 1 | A sound's title, artist, duration, cover, and how many videos use it | `clipId` | | `GET /v1/tiktok/song/videos` | 1 | The videos made with that sound, paged | `clipId`, `cursor` | `trending` has no keyword and no cursor, so there is nothing to page. The two sound endpoints together are how you tell a rising sound from a saturated one before you brief a creator on it. ### The song behind a sound Every TikTok video row carries `post.ext.matched_song_id`, `matched_song_title` and `matched_song_artist` when TikTok recognised the song a video plays. Creators often upload a track as their own "original sound", which gets a new `music_id` each time, so two uploads of the same song share a `matched_song_id` while their `music_id` values differ. Group rows on `matched_song_id` to count every use of one song. On 19/10/2026, `search/top?query=amapiano®ion=ZA` returned 30 videos and 22 carried a matched song. `post.ext.music_title`, `music_artist` and `music_is_original` describe the video's own sound. `song/videos` rows carry `post.ext.region`, the country each video is registered to. ### Which country the trending videos come from `trending` has two feeds, and `region` does something different on each. - **Default (`feed=global`).** TikTok's web trending feed, fetched through a connection in `region`. `region` sets where the request comes from, not where the videos come from, and for most countries the feed is one worldwide pool. On 13/09/2026 over 12 calls in six countries, 22% of its videos were registered in the requested country: US 65%, BR 43%, GB 9%, FR 8%, JP 6%, DE 0%. About 13 videos, 3 to 12 seconds. - **Local (`feed=local`).** The For You feed TikTok's app serves to a phone set to `region`. Most, not all, of its videos are from that country: 57% across 10 pages measured the same day (DE 52%, US 66%, GB 41%, JP 81%), and 51% were captioned in the country's main language. A median of 24 videos (13 to 33 on 9 pages in 10), and 9 calls in 10 answer within 22 seconds. When no video comes back the call returns `503` and refunds the credits, which happened on 1 of 25 test calls, so retry. A local call never falls back to the global feed. Both cost 5 credits and return each video once. Every row carries `post.ext.region`, the two-letter country the video is registered to, so when you need only one country, filter on it. Local rows also carry `post.ext.content_language`, TikTok's own caption-language tag, which is null when TikTok could not tell. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/trending?region=DE&feed=local" \ -H "x-api-key: YOUR_API_KEY" ``` ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/trending?region=US" \ -H "x-api-key: YOUR_API_KEY" curl "https://www.socialcrawl.dev/v1/tiktok/song/videos?clipId=7016187547651328773" \ -H "x-api-key: YOUR_API_KEY" ``` ### TikTok's own trend boards TikTok publishes ranked trend boards for advertisers, now part of TikTok One. Two of them are live, and you need nothing but a market and a time window to read them. | Endpoint | Credits | What it returns | Key parameters | | ---------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------- | | `GET /v1/tiktok/hashtags/popular` | 2 per hashtag, minimum 6 | TikTok's trending hashtags for a market: the overall board and 15 industry boards | `countryCode` (27 markets), `period`, `industry` | | `GET /v1/tiktok/videos/popular` | 25 per board plus 1 per video | TikTok's Top Videos board, ranked by views, engagement or 6-second views | `countryCode` (US, JP, VN, TH, ID), `period`, `orderBy`, `limit` | - **Hashtags.** TikTok shows three hashtags per board to a reader who is not logged in, so one board is three rows and costs 6 credits. Pass `industry=all` to read the overall board and all 15 industry boards in one call: 44 to 46 unique hashtags for Germany on 13/09/2026, for about 90 credits, in 3 to 4 seconds. Each row carries its rank, the window's post and view counts, a daily popularity curve and the top creators under `post.ext.trend`. A hashtag that ranks on more than one board comes back once and is billed once; `post.ext.trend.boards` lists every board it ranks on with its rank there (in Germany #marvelrivals was third overall and first in Games). `period` is 7, 30 or 90 days. - **Videos.** TikTok publishes this board for five markets only. A request for any other market, Germany included, returns `400` at no charge and points you to the hashtag board. These values from the old boards are gone and return `400` at no charge: a 120-day window, `newOnBoard`, a second page, and the business-services, financial-services and life-services industries. The popular songs board was removed by TikTok and the creator board shows "Coming soon", so `GET /v1/tiktok/songs/popular` and `GET /v1/tiktok/creators/popular` return `503` and never bill. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/hashtags/popular?countryCode=DE&period=7&industry=all" \ -H "x-api-key: YOUR_API_KEY" ``` ## Ad Library | Endpoint | Credits | What it returns | Key parameters | | --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `GET /v1/tiktok/adlibrary/search` | 5 to 17 | Ads matching a keyword or an advertiser; `include=ad` adds the brand, landing page and advertiser link per ad | `query` or `advertiser_name`, `include`, `limit` (1-12) | | `GET /v1/tiktok/adlibrary/ad` | 5 | One ad in full: creative, landing page, and brand | `ad_id` or `url` | Same advanced tier as the Facebook, Google, and LinkedIn ad libraries. ### Brand and landing page next to ad results The library's search row names the advertiser and carries the creative, the impression estimate, the spend band and the dates, and nothing about where the ad sends people: `post.ext.ad.brand_name`, `post.ext.ad.landing_page`, `post.ext.ad.profile_web_link` and `post.author.avatar_url` are null on a plain call. Add `include=ad` and every ad is filled from the per-ad record in the same call: the registered brand name, the landing page, the advertiser's TikTok profile link and avatar, and the objectives, countries and source where the library has them. 5 credits for the page plus 1 per ad filled from a fresh lookup, so 17 at most for the 12-ad page; ads already in cache are free, and ads that could not be filled are refunded and named in `_warnings`. `limit=N` (1 to 12) caps the ads and the extra credits together. The join adds 5 to 20 seconds on a fresh page (never more than 20), and the search itself is often the slow part of the call. Like, comment and share counts are not published by the library on either endpoint; for the performance side of TikTok advertising use `ads/top`, which reads the Creative Center board. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/tiktok/adlibrary/search?advertiser_name=Gymshark&include=ad" \ -H "x-api-key: YOUR_API_KEY" ``` ## All endpoints ## Platform notes **Follower counts are exact where TikTok exposes them.** `author.followers` is the unrounded integer when the source publishes one. When only the rounded public figure exists, the response sets `author.ext.followers_approximate` to `true`. **Repeat profile lookups replay a cached snapshot.** A second lookup of the same account within 15 minutes is served from cache at no charge. Send `Cache-Control: no-cache` when you need a guaranteed-fresh read. **Age and gender do not exist on this platform.** `user/audience` returns geography only, because TikTok publishes no public demographic breakdown for a creator's followers. **`region` routes a request; it does not filter results.** It is accepted on `profile/videos`, `post`, `search`, `search/top`, and `search/hashtag`, where it sends the request through that country, which changes what TikTok is willing to serve (a video blocked in one country loads from another) but not which country the results come from. `trending` requires it: on the default feed it is the same routing, and with `feed=local` it picks the country the For You feed is built for (see [Which country the trending videos come from](#which-country-the-trending-videos-come-from)). To keep only one country's trending videos, filter the rows on `post.ext.region`. **Identifiers are not interchangeable.** `profile`, `user/followers`, and `profile/full` take `handle` or `user_id`; `post`, `post/comments`, and `post/transcript` take the video `url`, and `video/screen-text` takes a video or photo post `url` (a `/video/` or `/photo/` link); the sound endpoints take `clipId`. **`trim=true` costs the same.** Most list endpoints accept it and return a lighter payload at the same price. **TikTok Shop moved.** Product details, reviews, shop search, and creator showcases now live under `/v1/tiktokshop/...`. The old `/v1/tiktok/shop` and `/v1/tiktok/user/showcase` paths still resolve for backward compatibility. ## Labels These lists label their rows by default, at no extra credit. On `/v1/tiktok/post/comments` and `/v1/tiktok/video/comment/replies`, each comment carries `sentiment`, `question`, `purchase_intent` and `complaint` on `computed.labels`. On `/v1/tiktok/profile/videos`, `/v1/tiktok/search/hashtag` and `/v1/tiktok/search`, each row carries `sponsored`, `intent` and `niche`. `/v1/tiktok/search` also scores each row's relevance to your `query` on `computed.relevance`, without dropping or reordering anything; add `relevance=filter` to drop the rows that are about something else, still free. Send `judgments=off` for the page without them. See [Labels](/docs/labels.md) for the field shapes, rows still `pending`, and the labels you can add with `label=`. ## Next steps - [Pagination](/docs/pagination.md): Cursor and page walks, and what has_more actually means. - [Credits](/docs/credits.md): What each tier costs and when a call is refunded. - [API reference](/docs/api-reference.md): Every parameter and response field, endpoint by endpoint. - [Instagram](/docs/instagram.md): The other short-form surface most crawls pair with TikTok. - [TikTok Shop](/docs/tiktokshop.md): Products, reviews, and creator showcases on their own namespace.