# SocialCrawl API: tiktok endpoints # Base URL: https://www.socialcrawl.dev # Auth: x-api-key header # Full docs: https://www.socialcrawl.dev/docs/tiktok ## GET /v1/tiktok/profile Get TikTok user profile Credit cost: 1 (standard) Parameters: - handle (optional, string): TikTok username without the @ symbol. - user_id (optional, string): TikTok numeric user ID. Use this for faster responses. - counts (optional, enum: app | web): `app` (default) or `web`. `web` returns the follower, following, likes and video counts TikTok's website shows (`statsV2` `followerCount`, `followingCount`, `heartCount`, `videoCount`) in place of the app's counters. Same price. `author.ext.counts_source` confirms which set was returned. - Constraint: one of handle, user_id (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/profile" \ --data-urlencode "handle=charlidamelio" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/profile/videos List TikTok user videos Credit cost: 1-5 (metered): 1 credit per page. The default labels (sponsored, intent, niche) are free. label=mention, quality, ai_style, ai_written, injection, or label=intent with offer=, holds 4 extra credits and refunds down to 1 credit per started 25 posts that were newly judged: posts already labelled are free, a page where nothing could be judged refunds the whole extra, and a cached page is free. With `max_pages`, `max_items` or `max_credits`, each page walked is billed at this price (a cached page is free), and `max_credits` caps the total. Parameters: - since (optional, string): Only videos published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Older videos are left off the page, and the page that reaches one ends the walk: `next_cursor` is not returned and `pagination.stopped_at` is `since`. Pinned videos sit out of date order and never end the walk. The page still costs what a page costs, so a daily poll pays for the pages it walks and no more. - stop_at_id (optional, string): The id (`post.id`) or URL (`post.url`) of the newest video you already hold. The page stops just before it: that video and everything after it are left off, `next_cursor` is not returned, and `pagination.stopped_at` is `known_id`. A pinned video never counts as the stop point. If it is not on this page the page is returned in full with its cursor, so keep walking. `pagination.stopped_at` is `end` when the list ran out first and `null` while there is more to walk. - handle (optional, string): TikTok username without the @ symbol - user_id (optional, string): TikTok user id. Use this for faster responses. - sort_by (optional, enum: latest | popular): Accepted for compatibility and ignored on this endpoint: neither `latest` nor `popular` reorders the page. Videos come back in the source's own order, described above. To rank a creator's videos by views or likes, page through with `max_cursor` and sort the collected rows yourself. - max_cursor (optional, string): Cursor to get more videos. Get 'max_cursor' from previous response. It is a Unix timestamp in milliseconds, and the page returns the videos posted before it, so you can also start from a date of your choice: `max_cursor=1590969600000` returns the videos posted just before 01/06/2020, then keep paging with each response's `max_cursor` (measured 22/10/2026: 27-31/05/2020 on khaby.lame, 26-31/12/2018 on nba). `until` only filters the rows of a page and does not move where the walk starts. - region (optional, string): Region (Country) you want the proxy in. Defaults to US. It does not filter the videos. The country TikTok registers each video to is post.ext.region. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - format (optional, enum: stationery): Optional. stationery files each video into wedding, birthday, birth, sympathy, seasonal greeting, or leaves it ungrouped. data.format_buckets gives the counts, the ungrouped share, and a by-day count when the page spans more than one day. A null save count stays null. brief= skips the walk when the page is not about that subject. No extra credits. - brief (optional, string): Optional, with format=stationery. The subject these videos should be about. When the page is not about it, data.format_buckets.skip_creator is true and the videos are not filed. - label (optional, string): Optional CSV of SocialCrawl labels to add to every post. Without this param every page already carries sponsored, intent and niche, free; label= adds the labels you name to them (the defaults keep running). sponsored, intent and niche are free when asked for too; mention, quality, ai_style, intent with offer=, and injection add 1 credit per started 25 newly judged posts. judgments=off (or label=none) turns the default labels off. sponsored: is the post a paid or gifted promotion (p, 0 to 1), did it carry a disclosure marker such as #ad or 광고 (disclosed), is it likely paid with no marker (undisclosed), and which of the accounts it mentions does it promote (brand, or null). These are signals to review, never a finding. mention (needs brand=): is the post about that brand rather than something that shares its name (about_brand, 0 to 1), how it feels about the brand on five levels (sentiment_level 0 to 4 and sentiment_score 0 to 1, null when the post is not about the brand), is it sarcastic, which aspect it talks about (taste_or_quality, price_or_value, availability_or_delivery, health_or_safety, advertising_or_campaign, customer_service, none), and did the author buy or use it (first_hand). intent: what the author is mainly doing (label: asking_for_recommendation, comparing_options, switching_away, complaining, promoting, news_or_discussion, other, or null when unsure, with confidence), whether they read as a potential buyer rather than a seller (buyer, seller), how pressing the need is (urgency, 0 to 3), and, when you pass offer=, whether your offer would plausibly help them (fits_offer). niche: which of the 33 niches of the published taxonomy sc-niche-v1 the caption belongs to, or personal_no_niche, or other (label, confidence, taxonomy), with label null when the caption is too thin to tell or the pick is unsure. It reads the caption only, not the video. quality: how much checkable detail the caption carries (fact_density 0 to 3), whether it mainly asks for likes, replies, shares, follows or tags (engagement_bait, 0 to 1), whether it is written to provoke anger as a way to get engagement (rage_bait, 0 to 1; about the writing, never the side taken), whether it only repeats someone else's news or view (secondhand, 0 to 1), and what the post is mainly doing (post_aim: inform, opinion, sell, entertain, provoke, other). exclude=engagement_bait drops posts whose engagement_bait is 0.8 or higher. ai_style: writing patterns common in AI-generated text, judged on the text alone. It does not say who wrote the post, and a person who writes in this style is flagged too. Six patterns are judged, each a probability (0 to 1): binary_contrast ("This is not X. It's Y."), faux_insight ("Here's what nobody tells you" and throat-clearing openers), colon_reveal ("The best part: it learns."), profound_kicker (ends on an aphorism or mic-drop line), puffery ("marks a pivotal moment", "highlighting the importance of") and rhetorical_setup ("What if I told you", a question the post answers itself, "Not a X. Not a Y. A Z."). Four are counted in the text: slop_words (words and phrases such as delve, leverage, robust, game changer, at the end of the day), em_dashes, emoji_bullets (lines that start with an emoji or a symbol bullet) and one_line_paragraphs (paragraphs that are one short line). pattern_count is how many patterns are present (a judged pattern at 0.8 or higher, slop_words 1 or more, em_dashes 2 or more, emoji_bullets 2 or more, one_line_paragraphs 5 or more) and markers lists their names, comma-separated. A post with less than about eight words of text is only counted, with every judged pattern 0. The judged patterns read the first 400 and the last 150 characters of a long post, and the counts read the whole post. exclude=ai_style drops posts with 2 or more patterns. ai_written: how likely the post was written by an AI model, from SocialCrawl's own text classifier, reading the opening of the post (about 1,000 characters): p (0 to 1, calibrated), level (likely (p 0.95 or higher), possible (p 0.71 or higher) or unlikely) and model (the model name; a new name means a new scale). A post under 8 words is too short to read: p null, level too_short. It is a probability, not proof: a person can be flagged and an AI-written post can pass, and the shorter the post the less certain it is. exclude=ai_written drops likely posts. injection flags text that addresses an AI system and tries to direct it (flagged, p); it never drops or rewrites a row. A post that could not be judged carries labels: null. Posts already labelled for anyone are free, and so is a cached page. data.labels reports what was judged and billed. - exclude (optional, string): Optional CSV (engagement_bait, ai_style, ai_written). engagement_bait, only with label=quality, drops posts whose engagement_bait probability is 0.8 or higher. ai_style, only with label=ai_style, drops posts that show 2 or more of its writing patterns (pattern_count 2 or higher). ai_written, only with label=ai_written, drops posts whose level is likely (p 0.95 or higher), or whose p reaches your ai_written_threshold; a post too short to read is never dropped. Dropped posts are listed in data.labels.dropped_ids. A post that could not be judged is never dropped. - ai_written_threshold (optional, string): Optional, only with label=ai_written and exclude=ai_written. The p (0 to 1) at which exclude=ai_written drops a post. Default 0.95 (level likely). 0.71 also drops possible posts: more AI posts go, and more posts by people with them. - brand (optional, string): Required by label=mention, ignored otherwise. The brand or product the posts are judged against, up to 300 characters, for example brand=Buldak. Without it label=mention is skipped with the warning label_mention_needs_brand and is not billed. - brand_description (optional, string): Optional, with label=mention. One plain sentence saying what the brand is, for example brand_description=Samyang's spicy instant noodle brand. Use it when the name is also an ordinary word or another company's name. - offer (optional, string): Optional, with label=intent. One or two plain sentences saying what you sell, up to 300 characters, for example offer=A web design agency that builds websites for small businesses. Adds fits_offer to every post; without it fits_offer is null. - judgments (optional, enum: on | off): Optional, on (the default) or off. By default every row gains free SocialCrawl judgments (computed.labels, and computed.relevance on search endpoints), reported in data.labels (mode default) and data.relevance (origin default), each with a status (complete, partial or skipped) and pending: the rows still being judged when the page was sent, which carry null now and are filled on your next call or cached read. Default judgments never add credits, never change an existing field, and never drop or reorder a row. off returns the page exactly as before, with none of those keys. label=none does the same. - dry_run (optional, enum: 1): Optional. When 1, return a cost preview for this labelled or relevance-filtered request without fetching the page or judging any row. data.estimate reports rows_expected, rows_cached, label_credits_min, label_credits_max and base_credits. 0 credits charged. - label_evidence (optional, enum: 1): Optional, only with label=. When 1, every labelled row also carries computed.labels_evidence. = { quote, sentence_index }: the sentence in the row that most clearly shows the label, copied verbatim. Absent or null when no single sentence shows it. - fit (optional, enum: goal): Optional. When goal, keep the rows and fields needed for the goal you pass in goal= (plus any that are uncertain, and the first and last), and replace the rest with a stub. data.held_back lists the held ids and a recall id that re-reads the full page from cache at no extra charge. Without this param the page is unchanged. - goal (optional, string): Required by fit=goal. What you are trying to do, in your own words, up to 300 characters. - fit_tokens (optional, integer): Optional, only with fit=goal. Soft cap on how much of the page to keep, in tokens. Uncertain blocks and the first and last block are kept even if they exceed it. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - Constraint: one of handle, user_id (at least one required) Worth sending: - label=sponsored: Marks each post that is paid or sponsored, at no extra credits; intent and niche are free too. - since=2026-09-01: Returns only posts after this date, so a daily poll pays only for the pages with new posts. - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/profile/videos" \ --data-urlencode "handle=charlidamelio" \ --data-urlencode "label=sponsored" \ --data-urlencode "since=2026-09-01" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/post Get TikTok post details Credit cost: 1 (standard) Parameters: - url (required): Full URL of the TikTok video - region (optional, string): Two-letter country code for the proxy region (US, GB, FR, PH). The primary source does not take a region, so it is honoured only when a fallback source serves the request; a region-locked video (commonly from the Philippines) that fails on the primary is retried on the fallback with this value. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - download_media (optional, boolean): Set to true to also download the video/images and get back durable hosted media URLs under `data.post.ext.download_media_urls` (`[{ post_id, cdn_url, type, cached }]`). Use these for archiving; the raw `media_urls` are short-lived signed CDN links that expire. Adds a few seconds of latency while the media is fetched. Availability is source-dependent and most responses will not carry it: when the serving source cannot host durable copies the field is absent, the response carries a `data._warnings` note saying so, and `data.post.content.media_urls` still carries the media. Worth sending: - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/post" \ --data-urlencode "url=https://www.tiktok.com/@stoolpresidente/video/7623818255903329566" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/post/comments List TikTok post comments Credit cost: 1-7 (metered): 1 credit per page, with the default labels (sentiment, question, purchase_intent, complaint) included free. label=spam, toxic or low_quality holds 4 extra credits and refunds down to 1 credit per started 25 comments that were newly judged: comments already labelled are free, a page where nothing could be judged refunds the whole extra, and a cached page is free. With `max_pages`, `max_items` or `max_credits`, each page walked is billed at this price (a cached page is free), and `max_credits` caps the total. Parameters: - url (required): Full URL of the TikTok video to fetch comments for - cursor (optional, string): Cursor to get more comments. Get 'cursor' from previous response. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - sort (optional, enum: top | recent): Optional. Without it, comments come in TikTok's own order (a relevance ranking, not newest first). `recent` returns them newest first by published_at; `top` returns them by like count. TikTok has no newest-first order of its own, so `recent` sorts the comments that were read: set scan_pages to read more of the thread. data.scan.complete is true only when the whole thread was read. - scan_pages (optional, integer): Optional, 1 to 3, default 1. Reads that many pages, drops comments repeated across pages, and returns them all in the `sort` order (TikTok's own order when sort is absent). Costs 1 credit for each page that added comments; a page of repeats is not billed. data.scan reports the pages read and billed and whether the thread ended. - label (optional, string): Optional CSV of SocialCrawl labels to add to every comment: sentiment, spam, question, purchase_intent, complaint, toxic, low_quality, injection. Without this param every page already carries sentiment, question, purchase_intent and complaint, free; label= adds the labels you name to them (the defaults keep running). Each comment gains computed.labels with probabilities (0 to 1), so you choose the cut-off; a comment that could not be judged carries labels: null. sentiment, question, purchase_intent and complaint are free when asked for too; spam, toxic, low_quality and injection add 1 credit per started 25 newly judged comments (comments already labelled for anyone are free, and so is a cached page). injection flags text that addresses an AI system and tries to direct it (flagged, p); it never drops or rewrites a row. judgments=off (or label=none) turns the default labels off. data.labels reports what was judged and billed. - exclude (optional, string): Optional CSV (spam, low_quality), only with the same preset in label. Drops comments whose probability is 0.8 or higher and lists their ids in data.labels.dropped_ids. A comment that could not be judged is never dropped. - judgments (optional, enum: on | off): Optional, on (the default) or off. By default every row gains free SocialCrawl judgments (computed.labels, and computed.relevance on search endpoints), reported in data.labels (mode default) and data.relevance (origin default), each with a status (complete, partial or skipped) and pending: the rows still being judged when the page was sent, which carry null now and are filled on your next call or cached read. Default judgments never add credits, never change an existing field, and never drop or reorder a row. off returns the page exactly as before, with none of those keys. label=none does the same. - dry_run (optional, enum: 1): Optional. When 1, return a cost preview for this labelled or relevance-filtered request without fetching the page or judging any row. data.estimate reports rows_expected, rows_cached, label_credits_min, label_credits_max and base_credits. 0 credits charged. - label_evidence (optional, enum: 1): Optional, only with label=. When 1, every labelled row also carries computed.labels_evidence. = { quote, sentence_index }: the sentence in the row that most clearly shows the label, copied verbatim. Absent or null when no single sentence shows it. - fit (optional, enum: goal): Optional. When goal, keep the rows and fields needed for the goal you pass in goal= (plus any that are uncertain, and the first and last), and replace the rest with a stub. data.held_back lists the held ids and a recall id that re-reads the full page from cache at no extra charge. Without this param the page is unchanged. - goal (optional, string): Required by fit=goal. What you are trying to do, in your own words, up to 300 characters. - fit_tokens (optional, integer): Optional, only with fit=goal. Soft cap on how much of the page to keep, in tokens. Uncertain blocks and the first and last block are kept even if they exceed it. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off the page, and the page that reaches one ends the walk: `next_cursor` is not returned and `pagination.stopped_at` is `since`. Sending it sets the newest-first sort shown in this endpoint's sort parameter (any other sort value is a free 400). The page still costs what a page costs, so you pay for the pages you walk and no more. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. Worth sending: - label=purchase_intent: Marks each comment that shows buying intent, at no extra credits; sentiment, question and complaint are free too. - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/post/comments" \ --data-urlencode "url=https://www.tiktok.com/@stoolpresidente/video/7623818255903329566" \ --data-urlencode "label=purchase_intent" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/video/comment/replies List TikTok comment replies Credit cost: 1-5 (metered): 1 credit per page, with the default labels (sentiment, question, purchase_intent, complaint) included free. label=spam, toxic or low_quality holds 4 extra credits and refunds down to 1 credit per started 25 comments that were newly judged: comments already labelled are free, a page where nothing could be judged refunds the whole extra, and a cached page is free. With `max_pages`, `max_items` or `max_credits`, each page walked is billed at this price (a cached page is free), and `max_credits` caps the total. Parameters: - comment_id (required): TikTok comment ID. This is the cid from the comments endpoint. - url (required): TikTok video URL. This is the url from the comments endpoint. - cursor (optional, integer): Cursor to get more replies. Get 'cursor' from previous response. - label (optional, string): Optional CSV of SocialCrawl labels to add to every comment: sentiment, spam, question, purchase_intent, complaint, toxic, low_quality, injection. Without this param every page already carries sentiment, question, purchase_intent and complaint, free; label= adds the labels you name to them (the defaults keep running). Each comment gains computed.labels with probabilities (0 to 1), so you choose the cut-off; a comment that could not be judged carries labels: null. sentiment, question, purchase_intent and complaint are free when asked for too; spam, toxic, low_quality and injection add 1 credit per started 25 newly judged comments (comments already labelled for anyone are free, and so is a cached page). injection flags text that addresses an AI system and tries to direct it (flagged, p); it never drops or rewrites a row. judgments=off (or label=none) turns the default labels off. data.labels reports what was judged and billed. - exclude (optional, string): Optional CSV (spam, low_quality), only with the same preset in label. Drops comments whose probability is 0.8 or higher and lists their ids in data.labels.dropped_ids. A comment that could not be judged is never dropped. - judgments (optional, enum: on | off): Optional, on (the default) or off. By default every row gains free SocialCrawl judgments (computed.labels, and computed.relevance on search endpoints), reported in data.labels (mode default) and data.relevance (origin default), each with a status (complete, partial or skipped) and pending: the rows still being judged when the page was sent, which carry null now and are filled on your next call or cached read. Default judgments never add credits, never change an existing field, and never drop or reorder a row. off returns the page exactly as before, with none of those keys. label=none does the same. - dry_run (optional, enum: 1): Optional. When 1, return a cost preview for this labelled or relevance-filtered request without fetching the page or judging any row. data.estimate reports rows_expected, rows_cached, label_credits_min, label_credits_max and base_credits. 0 credits charged. - label_evidence (optional, enum: 1): Optional, only with label=. When 1, every labelled row also carries computed.labels_evidence. = { quote, sentence_index }: the sentence in the row that most clearly shows the label, copied verbatim. Absent or null when no single sentence shows it. - fit (optional, enum: goal): Optional. When goal, keep the rows and fields needed for the goal you pass in goal= (plus any that are uncertain, and the first and last), and replace the rest with a stub. data.held_back lists the held ids and a recall id that re-reads the full page from cache at no extra charge. Without this param the page is unchanged. - goal (optional, string): Required by fit=goal. What you are trying to do, in your own words, up to 300 characters. - fit_tokens (optional, integer): Optional, only with fit=goal. Soft cap on how much of the page to keep, in tokens. Uncertain blocks and the first and last block are kept even if they exceed it. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. Worth sending: - label=purchase_intent: Marks each comment that shows buying intent, at no extra credits; sentiment, question and complaint are free too. curl -G "https://www.socialcrawl.dev/v1/tiktok/video/comment/replies" \ --data-urlencode "comment_id=7623828115408274207" \ --data-urlencode "url=https://www.tiktok.com/@stoolpresidente/video/7623818255903329566" \ --data-urlencode "label=purchase_intent" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/comment Look up one TikTok comment by URL or id Credit cost: 2-6 (metered): 2 credits for a standard lookup, 6 with `deep_scan=true`, which keeps paging the thread when the comment is not in the first pages. Reach for deep_scan only after a standard lookup comes back not-found Parameters: - comment_url (optional, string): A TikTok comment URL: `https://www.tiktok.com/@{handle}/video/{videoId}?comment_id={cid}`, the `m.tiktok.com/v/{id}.html?...&share_comment_id={cid}` share form, or a `vm.tiktok.com/{code}` / `tiktok.com/t/{code}` shortlink. The `cid`/`comment_id`/`share_comment_id` query value may be numeric or the base64 form TikTok's share sheet emits (`?cid=NzY1...`): both are accepted; base64 is decoded automatically. Mutually exclusive with `post_url`+`comment_id`. - post_url (optional, string): The post URL (`https://www.tiktok.com/@{handle}/video/{videoId}`). Combine with `comment_id`, or with `author_username`/`text_contains` for a search. - comment_id (optional, string): The target comment's numeric id (the `cid` from the comments endpoint). Requires `post_url`. Note that TikTok share links carry a base64-encoded `cid`: pass such a link as `comment_url` instead (decoded automatically), or base64-decode the value to its digits before passing it here. - parent_comment_id (optional, string): The parent comment's numeric id: supply this when the target is a reply so it can be resolved directly via the native replies endpoint. - author_username (optional, string): Return up to `max` comments authored by this username (no comment id needed). Mutually exclusive with `text_contains` and any comment id. - text_contains (optional, string): Return up to `max` comments whose text contains this snippet (case-insensitive). Mutually exclusive with `author_username` and any comment id. - deep_scan (optional, boolean): Widen the scan budget for deeply-buried comments (raises the page ceiling and deadline). Bills 6 credits instead of 2. - position_hint (optional, string): Opaque token from a prior lookup's `lookup.position_hint`. Passing it back probes the comment's last-known location first, making a re-check of an already-found comment cheap. - max (optional, integer): For `author_username`/`text_contains` search: max matches to return (1-20, default 5). - Constraint: one of comment_url, post_url (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/comment" \ --data-urlencode "comment_url=https://www.tiktok.com/@mrbeast/video/7654638524729216287?comment_id=7654640784985211670" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/search Search TikTok videos by keyword Credit cost: 1-42 (metered): 1 credit for the page, including any limit of 30 or less. A limit above 30 holds ceil(N/4)+2 credits for the N rows the source fills (60 to 120, rounded up to a multiple of 30), 17 at 60 and 32 at 120, and unfilled slots are refunded. Repeated videos are removed after billing. Relevance against your query (by default, or with relevance=) is free. Adding relevant_to= holds 5 extra credits and refunds down to 1 credit per started 25 rows that were newly judged: rows already judged for the same topic are free, a page where nothing could be judged refunds the whole extra, and a cached page is free. The default labels (sponsored, intent, niche) are free. label=mention, quality, ai_style, ai_written, injection, or label=intent with offer=, holds 5 extra credits and refunds down to 1 credit per started 25 posts that were newly judged: posts already labelled are free, a page where nothing could be judged refunds the whole extra, and a cached page is free. With `max_pages`, `max_items` or `max_credits`, each page walked is billed at this price (a cached page is free), and `max_credits` caps the total. With `seen`, a page's price falls with its share of rows you already received under that id; credits for joined data (`include=`) are never discounted. Parameters: - query (required): Search keyword or phrase to find TikTok videos - date_posted (optional, enum: yesterday | this-week | this-month | last-3-months | last-6-months | all-time): Time Frame - sort_by (optional, enum: relevance | most-liked | date-posted): Sort by - region (optional, string): Sets the proxy country (ISO 3166-1 alpha-2). It does not filter the page to that country. Each row already carries post.ext.region, the country the video is registered to; filter on that leaf when you need only one country. - cursor (optional, integer): Cursor to get more videos. Get 'cursor' from previous response. Ignored when `limit` is above 30: that call is a single page. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - limit (optional, integer): A limit of 30 or less returns the first limit rows of the normal page for 1 credit; next_cursor still steps the whole page of about 30, so the rest of that page is skipped. A limit above 30 selects the bulk labeled page: up to 120 videos in one call, each with post.ext.region. The value rounds up to the next multiple of 30 (60, 90 or 120). 17 credits at 60, 32 at 120; slots the source does not fill are refunded. date_posted and sort_by apply on the bulk page too. The source can repeat a video; repeats are removed, so a 120 page is often a little under 120 unique videos. Does not filter by country; drop rows yourself on post.ext.region. - expand (optional, boolean): Send `true` to keep paging past TikTok's own limit for one query, which ends a walk at about 200 to 400 videos. When your ranking runs out, the walk continues with TikTok's related searches for your query that contain every word of it (for `coffee`: `iced coffee at home`, `coffee cold brew recipe`) and then with the other two rankings, each to its end, keeping your `date_posted` and `region`. A video can come up again on a later step, so deduplicate on `post.id`. Rows after your own ranking are not in your `sort_by` order. Every page is 1 credit, as usual; page with `cursor`, or collect many pages in one call with `max_items`. - relevance (optional, enum: score | filter): Optional. Without this param every row already carries computed.relevance against your query, free ({ p, sense, depth, spam }: is this row about what your query means, or a different thing that shares its words?), and nothing is dropped or reordered. score asks for it explicitly and waits for every row; filter also drops the rows that are not about your query, and lists their ids in data.relevance.dropped_ids. Your query is the topic; nothing to configure. A row that could not be judged is never dropped and carries relevance: null. Pagination is unchanged, so a filtered page can hold fewer rows. Free with your query as the topic; with relevant_to it adds 1 credit per started 25 newly judged rows (rows already judged for the same topic are free, and so is a cached page). - relevance_threshold (optional, string): Optional, only with relevance. The probability (0 to 1) a row must reach to be kept by relevance=filter. Default 0.5. Lower keeps more rows, higher keeps fewer. - relevant_to (optional, string): Optional, only with relevance. Up to 200 characters describing what you mean, used as the topic instead of the query. Use it when the query is ambiguous, for example query=cleopatra with relevant_to=Cleopatra, the IGT slot game. - judgments (optional, enum: on | off): Optional, on (the default) or off. By default every row gains free SocialCrawl judgments (computed.labels, and computed.relevance on search endpoints), reported in data.labels (mode default) and data.relevance (origin default), each with a status (complete, partial or skipped) and pending: the rows still being judged when the page was sent, which carry null now and are filled on your next call or cached read. Default judgments never add credits, never change an existing field, and never drop or reorder a row. off returns the page exactly as before, with none of those keys. label=none does the same. - dry_run (optional, enum: 1): Optional. When 1, return a cost preview for this labelled or relevance-filtered request without fetching the page or judging any row. data.estimate reports rows_expected, rows_cached, label_credits_min, label_credits_max and base_credits. 0 credits charged. - fit (optional, enum: goal): Optional. When goal, keep the rows and fields needed for the goal you pass in goal= (plus any that are uncertain, and the first and last), and replace the rest with a stub. data.held_back lists the held ids and a recall id that re-reads the full page from cache at no extra charge. Without this param the page is unchanged. - goal (optional, string): Required by fit=goal. What you are trying to do, in your own words, up to 300 characters. - fit_tokens (optional, integer): Optional, only with fit=goal. Soft cap on how much of the page to keep, in tokens. Uncertain blocks and the first and last block are kept even if they exceed it. - label (optional, string): Optional CSV of SocialCrawl labels to add to every post. Without this param every page already carries sponsored, intent and niche, free; label= adds the labels you name to them (the defaults keep running). sponsored, intent and niche are free when asked for too; mention, quality, ai_style, intent with offer=, and injection add 1 credit per started 25 newly judged posts. judgments=off (or label=none) turns the default labels off. sponsored: is the post a paid or gifted promotion (p, 0 to 1), did it carry a disclosure marker such as #ad or 광고 (disclosed), is it likely paid with no marker (undisclosed), and which of the accounts it mentions does it promote (brand, or null). These are signals to review, never a finding. mention (needs brand=): is the post about that brand rather than something that shares its name (about_brand, 0 to 1), how it feels about the brand on five levels (sentiment_level 0 to 4 and sentiment_score 0 to 1, null when the post is not about the brand), is it sarcastic, which aspect it talks about (taste_or_quality, price_or_value, availability_or_delivery, health_or_safety, advertising_or_campaign, customer_service, none), and did the author buy or use it (first_hand). intent: what the author is mainly doing (label: asking_for_recommendation, comparing_options, switching_away, complaining, promoting, news_or_discussion, other, or null when unsure, with confidence), whether they read as a potential buyer rather than a seller (buyer, seller), how pressing the need is (urgency, 0 to 3), and, when you pass offer=, whether your offer would plausibly help them (fits_offer). niche: which of the 33 niches of the published taxonomy sc-niche-v1 the caption belongs to, or personal_no_niche, or other (label, confidence, taxonomy), with label null when the caption is too thin to tell or the pick is unsure. It reads the caption only, not the video. quality: how much checkable detail the caption carries (fact_density 0 to 3), whether it mainly asks for likes, replies, shares, follows or tags (engagement_bait, 0 to 1), whether it is written to provoke anger as a way to get engagement (rage_bait, 0 to 1; about the writing, never the side taken), whether it only repeats someone else's news or view (secondhand, 0 to 1), and what the post is mainly doing (post_aim: inform, opinion, sell, entertain, provoke, other). exclude=engagement_bait drops posts whose engagement_bait is 0.8 or higher. ai_style: writing patterns common in AI-generated text, judged on the text alone. It does not say who wrote the post, and a person who writes in this style is flagged too. Six patterns are judged, each a probability (0 to 1): binary_contrast ("This is not X. It's Y."), faux_insight ("Here's what nobody tells you" and throat-clearing openers), colon_reveal ("The best part: it learns."), profound_kicker (ends on an aphorism or mic-drop line), puffery ("marks a pivotal moment", "highlighting the importance of") and rhetorical_setup ("What if I told you", a question the post answers itself, "Not a X. Not a Y. A Z."). Four are counted in the text: slop_words (words and phrases such as delve, leverage, robust, game changer, at the end of the day), em_dashes, emoji_bullets (lines that start with an emoji or a symbol bullet) and one_line_paragraphs (paragraphs that are one short line). pattern_count is how many patterns are present (a judged pattern at 0.8 or higher, slop_words 1 or more, em_dashes 2 or more, emoji_bullets 2 or more, one_line_paragraphs 5 or more) and markers lists their names, comma-separated. A post with less than about eight words of text is only counted, with every judged pattern 0. The judged patterns read the first 400 and the last 150 characters of a long post, and the counts read the whole post. exclude=ai_style drops posts with 2 or more patterns. ai_written: how likely the post was written by an AI model, from SocialCrawl's own text classifier, reading the opening of the post (about 1,000 characters): p (0 to 1, calibrated), level (likely (p 0.95 or higher), possible (p 0.71 or higher) or unlikely) and model (the model name; a new name means a new scale). A post under 8 words is too short to read: p null, level too_short. It is a probability, not proof: a person can be flagged and an AI-written post can pass, and the shorter the post the less certain it is. exclude=ai_written drops likely posts. injection flags text that addresses an AI system and tries to direct it (flagged, p); it never drops or rewrites a row. A post that could not be judged carries labels: null. Posts already labelled for anyone are free, and so is a cached page. data.labels reports what was judged and billed. - exclude (optional, string): Optional CSV (engagement_bait, ai_style, ai_written). engagement_bait, only with label=quality, drops posts whose engagement_bait probability is 0.8 or higher. ai_style, only with label=ai_style, drops posts that show 2 or more of its writing patterns (pattern_count 2 or higher). ai_written, only with label=ai_written, drops posts whose level is likely (p 0.95 or higher), or whose p reaches your ai_written_threshold; a post too short to read is never dropped. Dropped posts are listed in data.labels.dropped_ids. A post that could not be judged is never dropped. - ai_written_threshold (optional, string): Optional, only with label=ai_written and exclude=ai_written. The p (0 to 1) at which exclude=ai_written drops a post. Default 0.95 (level likely). 0.71 also drops possible posts: more AI posts go, and more posts by people with them. - brand (optional, string): Required by label=mention, ignored otherwise. The brand or product the posts are judged against, up to 300 characters, for example brand=Buldak. Without it label=mention is skipped with the warning label_mention_needs_brand and is not billed. - brand_description (optional, string): Optional, with label=mention. One plain sentence saying what the brand is, for example brand_description=Samyang's spicy instant noodle brand. Use it when the name is also an ordinary word or another company's name. - offer (optional, string): Optional, with label=intent. One or two plain sentences saying what you sell, up to 300 characters, for example offer=A web design agency that builds websites for small businesses. Adds fits_offer to every post; without it fits_offer is null. - label_evidence (optional, enum: 1): Optional, only with label=. When 1, every labelled row also carries computed.labels_evidence. = { quote, sentence_index }: the sentence in the row that most clearly shows the label, copied verbatim. Absent or null when no single sentence shows it. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - min_views (optional, integer): Keep only rows with at least this many views (`post.engagement.views`). A row whose view count is unknown is discarded, so every returned row meets the floor. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - max_age_days (optional, integer): Keep only rows published within this many days (`post.published_at`), 1 to 3650. A row with no date is discarded. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - country (optional, string): Comma-separated two-letter ISO 3166-1 codes, for example `US,GB`. Keep only videos TikTok registers to one of these countries (`post.ext.region`); a row with no country is discarded. This filters rows; it does not change where the search runs (that is `region`). Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - exclude_country (optional, string): Comma-separated two-letter ISO 3166-1 codes, for example `IN`. Discard videos TikTok registers to any of these countries (`post.ext.region`); a row with no country is kept. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - sort_rows (optional, enum: views): `views`: return the kept rows ordered by view count, highest first, across every page walked. Rows without a view count go last. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them (after any filter). Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows (after any filter), then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - seen (optional, string): An id you choose (1 to 64 letters, digits, `.`, `_` or `-`). Rows your account already received under the same `seen` id are removed from the page, and the page price falls with the share of repeats: page credits x new rows / rows on the page, rounded up, so a page of nothing but repeats is free. Only the page's own price is discounted: credits for joined data (`include=`) are charged only for rows you have not received, and are never discounted. The memory lasts 24 hours from the last call that used the id, holds row ids only, and is private to your account. Use one id across a set of related searches; `data.walk.repeats` counts what was removed. Worth sending: - label=sponsored: Marks each post that is paid or sponsored, at no extra credits; intent and niche are free too. - relevance=filter: Drops rows that are not about your query, at no extra credits; relevance=score keeps them and adds a score. - date_posted=yesterday: Only posts from this time window. curl -G "https://www.socialcrawl.dev/v1/tiktok/search" \ --data-urlencode "query=cooking recipes" \ --data-urlencode "label=sponsored" \ --data-urlencode "relevance=filter" \ --data-urlencode "date_posted=yesterday" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/trending Get TikTok trending feed Credit cost: 5 (advanced) Parameters: - region (required): ISO 3166-1 alpha-2 country code (e.g., US, GB, DE). On the default feed it sets the country the request is routed through and does not filter videos; with `feed=local` it sets the country the For You feed is built for. Either way, check `ext.region` on each row for the country a video is actually from. - trim (optional, boolean): Accepted for compatibility; it has no effect with `feed=local`. - feed (optional, enum: global | local): `global` (default): the web trending feed routed through `region`, mostly worldwide content, about 13 videos in 3 to 12 seconds. `local`: the For You feed for a phone in `region`, most videos from that country, a median of 24 videos with 9 calls in 10 answering within 22 seconds, and a refunded 503 when none come back. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. Worth sending: - region=US: Results for one market. - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/trending" \ --data-urlencode "region=US" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/search/hashtag Search TikTok by hashtag Credit cost: 1-5 (metered): 1 credit per page. The default labels (sponsored, intent, niche) are free. label=mention, quality, ai_style, ai_written, injection, or label=intent with offer=, holds 4 extra credits and refunds down to 1 credit per started 25 posts that were newly judged: posts already labelled are free, a page where nothing could be judged refunds the whole extra, and a cached page is free. With `max_pages`, `max_items` or `max_credits`, each page walked is billed at this price (a cached page is free), and `max_credits` caps the total. With `seen`, a page's price falls with its share of rows you already received under that id; credits for joined data (`include=`) are never discounted. Parameters: - hashtag (required): Hashtag to search for without the # symbol - region (optional, string): Region the proxy will be set to. Note: this isn't going to grab you all tiktoks from this region, you're just setting the proxy there. - cursor (optional, integer): Cursor to get more videos. Get 'cursor' from previous response. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - label (optional, string): Optional CSV of SocialCrawl labels to add to every post. Without this param every page already carries sponsored, intent and niche, free; label= adds the labels you name to them (the defaults keep running). sponsored, intent and niche are free when asked for too; mention, quality, ai_style, intent with offer=, and injection add 1 credit per started 25 newly judged posts. judgments=off (or label=none) turns the default labels off. sponsored: is the post a paid or gifted promotion (p, 0 to 1), did it carry a disclosure marker such as #ad or 광고 (disclosed), is it likely paid with no marker (undisclosed), and which of the accounts it mentions does it promote (brand, or null). These are signals to review, never a finding. mention (needs brand=): is the post about that brand rather than something that shares its name (about_brand, 0 to 1), how it feels about the brand on five levels (sentiment_level 0 to 4 and sentiment_score 0 to 1, null when the post is not about the brand), is it sarcastic, which aspect it talks about (taste_or_quality, price_or_value, availability_or_delivery, health_or_safety, advertising_or_campaign, customer_service, none), and did the author buy or use it (first_hand). intent: what the author is mainly doing (label: asking_for_recommendation, comparing_options, switching_away, complaining, promoting, news_or_discussion, other, or null when unsure, with confidence), whether they read as a potential buyer rather than a seller (buyer, seller), how pressing the need is (urgency, 0 to 3), and, when you pass offer=, whether your offer would plausibly help them (fits_offer). niche: which of the 33 niches of the published taxonomy sc-niche-v1 the caption belongs to, or personal_no_niche, or other (label, confidence, taxonomy), with label null when the caption is too thin to tell or the pick is unsure. It reads the caption only, not the video. quality: how much checkable detail the caption carries (fact_density 0 to 3), whether it mainly asks for likes, replies, shares, follows or tags (engagement_bait, 0 to 1), whether it is written to provoke anger as a way to get engagement (rage_bait, 0 to 1; about the writing, never the side taken), whether it only repeats someone else's news or view (secondhand, 0 to 1), and what the post is mainly doing (post_aim: inform, opinion, sell, entertain, provoke, other). exclude=engagement_bait drops posts whose engagement_bait is 0.8 or higher. ai_style: writing patterns common in AI-generated text, judged on the text alone. It does not say who wrote the post, and a person who writes in this style is flagged too. Six patterns are judged, each a probability (0 to 1): binary_contrast ("This is not X. It's Y."), faux_insight ("Here's what nobody tells you" and throat-clearing openers), colon_reveal ("The best part: it learns."), profound_kicker (ends on an aphorism or mic-drop line), puffery ("marks a pivotal moment", "highlighting the importance of") and rhetorical_setup ("What if I told you", a question the post answers itself, "Not a X. Not a Y. A Z."). Four are counted in the text: slop_words (words and phrases such as delve, leverage, robust, game changer, at the end of the day), em_dashes, emoji_bullets (lines that start with an emoji or a symbol bullet) and one_line_paragraphs (paragraphs that are one short line). pattern_count is how many patterns are present (a judged pattern at 0.8 or higher, slop_words 1 or more, em_dashes 2 or more, emoji_bullets 2 or more, one_line_paragraphs 5 or more) and markers lists their names, comma-separated. A post with less than about eight words of text is only counted, with every judged pattern 0. The judged patterns read the first 400 and the last 150 characters of a long post, and the counts read the whole post. exclude=ai_style drops posts with 2 or more patterns. ai_written: how likely the post was written by an AI model, from SocialCrawl's own text classifier, reading the opening of the post (about 1,000 characters): p (0 to 1, calibrated), level (likely (p 0.95 or higher), possible (p 0.71 or higher) or unlikely) and model (the model name; a new name means a new scale). A post under 8 words is too short to read: p null, level too_short. It is a probability, not proof: a person can be flagged and an AI-written post can pass, and the shorter the post the less certain it is. exclude=ai_written drops likely posts. injection flags text that addresses an AI system and tries to direct it (flagged, p); it never drops or rewrites a row. A post that could not be judged carries labels: null. Posts already labelled for anyone are free, and so is a cached page. data.labels reports what was judged and billed. - exclude (optional, string): Optional CSV (engagement_bait, ai_style, ai_written). engagement_bait, only with label=quality, drops posts whose engagement_bait probability is 0.8 or higher. ai_style, only with label=ai_style, drops posts that show 2 or more of its writing patterns (pattern_count 2 or higher). ai_written, only with label=ai_written, drops posts whose level is likely (p 0.95 or higher), or whose p reaches your ai_written_threshold; a post too short to read is never dropped. Dropped posts are listed in data.labels.dropped_ids. A post that could not be judged is never dropped. - ai_written_threshold (optional, string): Optional, only with label=ai_written and exclude=ai_written. The p (0 to 1) at which exclude=ai_written drops a post. Default 0.95 (level likely). 0.71 also drops possible posts: more AI posts go, and more posts by people with them. - brand (optional, string): Required by label=mention, ignored otherwise. The brand or product the posts are judged against, up to 300 characters, for example brand=Buldak. Without it label=mention is skipped with the warning label_mention_needs_brand and is not billed. - brand_description (optional, string): Optional, with label=mention. One plain sentence saying what the brand is, for example brand_description=Samyang's spicy instant noodle brand. Use it when the name is also an ordinary word or another company's name. - offer (optional, string): Optional, with label=intent. One or two plain sentences saying what you sell, up to 300 characters, for example offer=A web design agency that builds websites for small businesses. Adds fits_offer to every post; without it fits_offer is null. - judgments (optional, enum: on | off): Optional, on (the default) or off. By default every row gains free SocialCrawl judgments (computed.labels, and computed.relevance on search endpoints), reported in data.labels (mode default) and data.relevance (origin default), each with a status (complete, partial or skipped) and pending: the rows still being judged when the page was sent, which carry null now and are filled on your next call or cached read. Default judgments never add credits, never change an existing field, and never drop or reorder a row. off returns the page exactly as before, with none of those keys. label=none does the same. - dry_run (optional, enum: 1): Optional. When 1, return a cost preview for this labelled or relevance-filtered request without fetching the page or judging any row. data.estimate reports rows_expected, rows_cached, label_credits_min, label_credits_max and base_credits. 0 credits charged. - label_evidence (optional, enum: 1): Optional, only with label=. When 1, every labelled row also carries computed.labels_evidence. = { quote, sentence_index }: the sentence in the row that most clearly shows the label, copied verbatim. Absent or null when no single sentence shows it. - fit (optional, enum: goal): Optional. When goal, keep the rows and fields needed for the goal you pass in goal= (plus any that are uncertain, and the first and last), and replace the rest with a stub. data.held_back lists the held ids and a recall id that re-reads the full page from cache at no extra charge. Without this param the page is unchanged. - goal (optional, string): Required by fit=goal. What you are trying to do, in your own words, up to 300 characters. - fit_tokens (optional, integer): Optional, only with fit=goal. Soft cap on how much of the page to keep, in tokens. Uncertain blocks and the first and last block are kept even if they exceed it. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - min_views (optional, integer): Keep only rows with at least this many views (`post.engagement.views`). A row whose view count is unknown is discarded, so every returned row meets the floor. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - max_age_days (optional, integer): Keep only rows published within this many days (`post.published_at`), 1 to 3650. A row with no date is discarded. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - sort_rows (optional, enum: views): `views`: return the kept rows ordered by view count, highest first, across every page walked. Rows without a view count go last. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them (after any filter). Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows (after any filter), then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - seen (optional, string): An id you choose (1 to 64 letters, digits, `.`, `_` or `-`). Rows your account already received under the same `seen` id are removed from the page, and the page price falls with the share of repeats: page credits x new rows / rows on the page, rounded up, so a page of nothing but repeats is free. Only the page's own price is discounted: credits for joined data (`include=`) are charged only for rows you have not received, and are never discounted. The memory lasts 24 hours from the last call that used the id, holds row ids only, and is private to your account. Use one id across a set of related searches; `data.walk.repeats` counts what was removed. Worth sending: - label=sponsored: Marks each post that is paid or sponsored, at no extra credits; intent and niche are free too. - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/search/hashtag" \ --data-urlencode "hashtag=fyp" \ --data-urlencode "label=sponsored" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/search/top TikTok top search results Credit cost: 1 (standard) Parameters: - query (required): Search keyword or phrase - publish_time (optional, enum: yesterday | this-week | this-month | last-3-months | last-6-months | all-time): Time Frame TikTok was posted - sort_by (optional, enum: relevance | most-liked | date-posted): Sort by - region (optional, string): Sets the proxy country (ISO 3166-1 alpha-2). It does not filter the page to that country. Each row already carries post.ext.region, the country the video is registered to; filter on that leaf when you need only one country. - cursor (optional, integer): Cursor to get more videos. Get 'cursor' from previous response. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - min_views (optional, integer): Keep only rows with at least this many views (`post.engagement.views`). A row whose view count is unknown is discarded, so every returned row meets the floor. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - max_age_days (optional, integer): Keep only rows published within this many days (`post.published_at`), 1 to 3650. A row with no date is discarded. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - country (optional, string): Comma-separated two-letter ISO 3166-1 codes, for example `US,GB`. Keep only videos TikTok registers to one of these countries (`post.ext.region`); a row with no country is discarded. This filters rows; it does not change where the search runs (that is `region`). Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - exclude_country (optional, string): Comma-separated two-letter ISO 3166-1 codes, for example `IN`. Discard videos TikTok registers to any of these countries (`post.ext.region`); a row with no country is kept. Filtering runs on our side after each page is fetched, so every page walked is billed as usual; `data.walk.discarded` counts what was removed and why. Each page is its own entry in `GET /v1/credits/transactions`, under the page's request id listed in `data.walk.request_ids`. - sort_rows (optional, enum: views): `views`: return the kept rows ordered by view count, highest first, across every page walked. Rows without a view count go last. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them (after any filter). Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows (after any filter), then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - seen (optional, string): An id you choose (1 to 64 letters, digits, `.`, `_` or `-`). Rows your account already received under the same `seen` id are removed from the page, and the page price falls with the share of repeats: page credits x new rows / rows on the page, rounded up, so a page of nothing but repeats is free. Only the page's own price is discounted: credits for joined data (`include=`) are charged only for rows you have not received, and are never discounted. The memory lasts 24 hours from the last call that used the id, holds row ids only, and is private to your account. Use one id across a set of related searches; `data.walk.repeats` counts what was removed. curl -G "https://www.socialcrawl.dev/v1/tiktok/search/top" \ --data-urlencode "query=dance challenge" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/search/users Search TikTok users Credit cost: 1-100 (metered): 1 credit for the page. include=profile holds 1 credit per row, at most 30 (or `limit`), and keeps only the rows filled from a fresh lookup, so a page is at most 31 credits; rows served from cache are free, unfilled rows are refunded, and a repeat within the cache window costs 0. country= holds 5 credits per requested in-country row (8 by default, at most 20) and keeps only the rows returned, so a miss is 0. The page is cached for 2 minutes and the per-account lookups for 15 minutes. With `max_pages`, `max_items` or `max_credits`, each page walked is billed at this price (a cached page is free), and `max_credits` caps the total. Parameters: - query (required): Search keyword or phrase to find TikTok users - cursor (optional, integer): Cursor to get more users. Get 'cursor' from previous response. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - include (optional, enum: profile): Set to `profile` (one token only) to fill `author.bio`, `author.location` (the ISO region), `author.ext.bio_link` and `author.ext.business_category` on every row in this one call. Holds 1 credit per row (at most 30, or `limit`) and keeps only the rows filled from a fresh lookup; rows already in cache and rows that could not be filled are refunded, so the most a call can cost is 1 + rows. Adds 4 to 9 seconds on a fresh page (never more than 12) and nothing when the rows are already cached; read `data.hydration` for the rows, the credits held and kept, and the time. - limit (optional, integer): Take the top N rows of the page (1 to 30) after the search has run. With `include=profile` it also caps the extra credits at N: `limit=5&include=profile` costs at most 6. With `country=` it is the in-country page size (1 to 20, default 8) and the credit cap at 5 per row. On the global page it is not a page size: `next_cursor` still advances past the full page, so rows beyond N on this page are not returned by the next page. - country (optional, enum: US | GB | CA | AU | NZ | IE | DE | FR | ES | IT | PT | NL | BE | CH | AT | SE | NO | DK | FI | IS | PL | CZ | SK | HU | RO | BG | GR | HR | RS | SI | UA | RU | TR | IL | AE | SA | QA | KW | EG | MA | ZA | NG | KE | GH | TZ | ET | BR | MX | AR | CO | CL | PE | VE | EC | UY | PY | BO | CR | PA | DO | JM | JP | KR | CN | TW | HK | MO | SG | MY | ID | TH | VN | PH | IN | PK | BD | LK | NP | MM | KH): ISO 3166-1 alpha-2 account region to filter by (KR, US, DE, …). Returns only creators whose public region is that country, 5 credits per row returned (8 by default, at most 20). A row is also dropped when neither its matched video, bio nor name is about your query, so a query that names only a market or a kind of account ("influencers") returns an empty page at 0 credits; put the topic in `query`. Omit it for the global 1-credit page. A code this endpoint does not accept is a free 400. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. Worth sending: - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/search/users" \ --data-urlencode "query=cooking" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/similar List similar TikTok accounts Credit cost: 5 credits a call. An empty list is refunded, an unknown handle is a free 404, and the list is cached for 15 minutes, so a repeat within that window costs 0. Parameters: - handle (required): TikTok username of the seed creator, without the @ symbol. curl -G "https://www.socialcrawl.dev/v1/tiktok/similar" \ --data-urlencode "handle=gordonramsayofficial" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/user/audience Get TikTok user audience demographics Credit cost: 5 (advanced) Parameters: - handle (required): TikTok username without the @ symbol curl -G "https://www.socialcrawl.dev/v1/tiktok/user/audience" \ --data-urlencode "handle=charlidamelio" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/user/followers List TikTok user followers Credit cost: 1 (standard) Parameters: - handle (optional, string): TikTok username without the @ symbol - user_id (optional, string): User id. Use this for faster response times. - min_time (optional, integer): Used to paginate. Get 'min_time' from previous response. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - Constraint: one of handle, user_id (at least one required) Worth sending: - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/user/followers" \ --data-urlencode "handle=stoolpresidente" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/user/following List TikTok user following Credit cost: 1 (standard) Parameters: - handle (optional, string): TikTok username without the @ symbol - user_id (optional, string): TikTok user id. Use this for faster responses. - min_time (optional, integer): Used to paginate. Get 'min_time' from previous response. - trim (optional, boolean): Accepted for compatibility; the response is already the canonical shape, so this flag has no effect. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - Constraint: one of handle, user_id (at least one required) Worth sending: - trim=true: Same price, lighter payload: drops the bulky raw fields. curl -G "https://www.socialcrawl.dev/v1/tiktok/user/following" \ --data-urlencode "handle=stoolpresidente" \ --data-urlencode "trim=true" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/user/live Get TikTok user live stream Credit cost: 1 (standard) Parameters: - handle (required): TikTok username without the @ symbol curl -G "https://www.socialcrawl.dev/v1/tiktok/user/live" \ --data-urlencode "handle=charlidamelio" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/post/transcript Get TikTok video transcript Credit cost: 10 (premium) Parameters: - url (required): Full URL of the TikTok video - language (optional, string): Language of the transcript. 2 letter language code, ie 'en', 'es', 'fr', 'de', 'it', 'ja', 'ko', 'zh'. Selects the caption track, and is passed to AI transcription of a longer video as the spoken language. Without it, AI transcription detects the language itself and can mistake a less common one for English, so set it when you know the language. If AI transcription of a longer video cannot transcribe the language you set, the request returns 404 RESOURCE_NOT_FOUND with `error.details.reason: language_unsupported` at 0 credits rather than a transcript in another language; retrying returns the same result. - use_ai_as_fallback (optional, string): Set to 'true' to fall back to AI transcription when the video has no captions. Included in the endpoint's 10 credits (no surcharge). AI transcription covers videos up to 10 minutes (600 seconds): a longer video with no captions returns 400 INVALID_REQUEST with `error.details.reason: video_too_long`, `duration_seconds` and `max_seconds`, free and deterministic, so retrying returns the same answer. - moments (optional, string): Optional CSV of moment kinds to find: brand, sponsor_read, product_demo, call_to_action. Returns data.moments = [{ kind, start_ms, end_ms, quote }] when the transcript has timestamps. Price of the extra step is an owner decision; today it does not add credits. - brand (optional, string): Optional, with moments=. The company or product to look for, so a namesake is not counted. - q (optional, string): Optional question about the video, up to 300 characters, for example q=what discount code do they give. Returns data.moments = [{ kind: "answers_query", start_ms, end_ms, quote }] for each stretch of about six transcript lines that states the answer: a stretch that only shares the topic is left out, and a question the video never answers returns an empty list. The quote is one line copied from the transcript and the times come from the segments. Send moments= or q=, not both. If the step cannot run, the transcript is returned without data.moments and data._warnings carries moments_unavailable. Today it does not add credits. curl -G "https://www.socialcrawl.dev/v1/tiktok/post/transcript" \ --data-urlencode "url=https://www.tiktok.com/@stoolpresidente/video/7499229683859426602" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/video/screen-text Get TikTok video on-screen text Credit cost: 5-10 (metered): 5 credits a call, the same for a video's cover or a photo post page of 10 slides. 10 credits with `frames=all` on a video; when the post is a photo post, or the video cannot be read and the cover is read instead, the call costs 5 and the response says so in `warnings`. Refunded when the post cannot be found, when nothing on it could be read, or when a parameter does not fit the post (a `slide_offset` past the last slide or on a video, a video over 180 seconds with `frames=all`). Parameters: - url (required): Full URL of the TikTok video or photo post (a `/video/` or `/photo/` link) - frames (optional, enum: cover | all): What to read on a video. `cover` (the default) reads the cover frame for 5 credits. `all` reads the whole video, up to 180 seconds, for 10 credits, and adds `subtitles`, `timeline` and `duration_seconds`. Has no effect on photo posts, which are read as usual for 5 credits. - slide_offset (optional, integer): Photo posts only: the 0-based index of the first slide to read, 10 slides a call. Default 0. `slide_offset=10` reads slides 11 to 20. Each slide in `images` keeps its index in the whole post, and `next_slide_offset` gives the value for the next call. curl -G "https://www.socialcrawl.dev/v1/tiktok/video/screen-text" \ --data-urlencode "url=https://www.tiktok.com/@stoolpresidente/video/7499229683859426602" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/song Get TikTok song details Credit cost: 1 (standard) Parameters: - clipId (required): TikTok sound/song clip ID curl -G "https://www.socialcrawl.dev/v1/tiktok/song" \ --data-urlencode "clipId=7439295283975702544" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/song/videos List TikTok videos using a song Credit cost: 1 (standard) Parameters: - clipId (optional, string): TikTok sound/song clip ID - cursor (optional, integer): The cursor to get the next page of results. - use (optional, boolean): Optional. 1 adds data.adoption.uses, a count of what the captions are doing with the sound. Omit it and the date histogram is still returned. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - Constraint: one of clipId (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/song/videos" \ --data-urlencode "clipId=7439295283975702544" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/hashtags/popular Read TikTok's own trending-hashtag board for a market and time window: the overall board plus 15 industry boards. Metered: 2 credits per hashtag returned, minimum 6 Credit cost: 6-96 (metered): 2 credits per hashtag returned, minimum 6. One board is 6 credits. industry=all holds 96 credits (sixteen boards of three) and settles down to the hashtags actually returned, typically 88 to 92. An empty board costs nothing. Parameters: - countryCode (optional, enum: US | FR | DE | IT | ES | GB | AR | AU | BR | CA | CO | EG | ID | IL | JP | KR | MY | MX | PH | SA | SG | ZA | TW | TH | TR | AE | VN): Market to read the board for, as an ISO country code. One of the 27 markets TikTok publishes the board for. Defaults to US. - period (optional, enum: 7 | 30 | 90): Window in days: 7, 30 or 90. Defaults to 7. Each window is a different board, not a longer list. - industry (optional, enum: all | education | vehicle-and-transportation | baby-kids-and-maternity | beauty-and-personal-care | tech-and-electronics | travel | household-products | pets | home-improvement | apparel-and-accessories | news-and-entertainment | games | food-and-beverage | sports-and-outdoor | health): Which board to read. Omit for the overall board (3 hashtags). Pass an industry slug for that industry's board (3 hashtags), or `all` for the overall board plus all 15 industry boards in one call. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. curl "https://www.socialcrawl.dev/v1/tiktok/hashtags/popular" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/videos/popular Read TikTok's own Top Videos board for the US, Japan, Vietnam, Thailand or Indonesia. Metered: 25 credits per board plus 1 per video returned Credit cost: 26-45 (metered): 25 credits per board plus 1 credit per video returned. The default limit of 20 holds 45 credits and settles to the videos actually returned; an empty board costs nothing. Parameters: - countryCode (optional, enum: US | JP | VN | TH | ID): Market: US, JP, VN, TH or ID, the only markets TikTok publishes this board for. Defaults to US. - period (optional, enum: 7 | 30): Window in days: 7 or 30. Defaults to 7. - orderBy (optional, enum: views | engagement | six_second_views): How TikTok ranks the board: views (the default), engagement, or six_second_views. - limit (optional, integer): How many videos to return, 1-20, default 20. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. curl "https://www.socialcrawl.dev/v1/tiktok/videos/popular" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/profile/region Get TikTok profile region Credit cost: 1 (standard) Parameters: - handle (required): TikTok username without the @ symbol curl -G "https://www.socialcrawl.dev/v1/tiktok/profile/region" \ --data-urlencode "handle=stoolpresidente" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/adlibrary/search Search the TikTok Ad Library Credit cost: 5-17 (metered): 5 credits for the page. include=ad holds 1 credit per ad, at most 12 (or `limit`), and keeps only the ads filled from a fresh lookup, so a page is at most 17 credits; ads served from cache are free, unfilled ads are refunded, and a repeat within the cache window costs 0. The page is cached for 2 minutes and the per-ad lookups for 30 minutes. With `max_pages`, `max_items` or `max_credits`, each page walked is billed at this price (a cached page is free), and `max_credits` caps the total. Parameters: - query (optional, string): Keyword or phrase to search the TikTok Ad Library. - advertiser_name (optional, string): Advertiser name as it appears in the TikTok Ad Library. - cursor (optional, string): Cursor from the previous response to fetch the next page. - include (optional, enum: ad): Set to `ad` (one token only) to fill the registered brand name, the landing page, the advertiser's TikTok profile link and avatar (`post.ext.ad.brand_name`, `post.ext.ad.landing_page`, `post.ext.ad.profile_web_link`, `post.author.avatar_url`), plus the objectives, countries and source where the library has them, on every ad in this one call. Holds 1 credit per ad (at most 12, or `limit`) and keeps only the ads filled from a fresh lookup; ads already in cache and ads that could not be filled are refunded, so the most a call can cost is 5 + ads. Adds 5 to 20 seconds on a fresh page (never more than 20) and nothing when the ads are already cached; read `data.hydration` for the rows, the credits held and kept, and the time. - limit (optional, integer): Take the top N ads of the page (1 to 12) after the search has run. With `include=ad` it also caps the extra credits at N: `limit=3&include=ad` costs at most 8. It is not a page size: `next_cursor` still advances past the full page. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - Constraint: one of query, advertiser_name (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/adlibrary/search" \ --data-urlencode "advertiser_name=Gymshark" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/adlibrary/ad Get a TikTok Ad Library ad Credit cost: 5 (advanced) Parameters: - ad_id (optional, string): TikTok Ad Library ad id, or a library.tiktok.com ads/detail URL. - url (optional, string): TikTok Ad Library ads/detail URL. - Constraint: one of ad_id, url (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/adlibrary/ad" \ --data-urlencode "ad_id=1869335220395266" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/search/suggestions Get TikTok search suggestions Credit cost: 1 (standard) Parameters: - query (required): Partial search query to autocomplete. - region (optional, string): ISO 3166-1 alpha-2 country code to localize suggestions. curl -G "https://www.socialcrawl.dev/v1/tiktok/search/suggestions" \ --data-urlencode "query=dogs" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/collection/videos List videos in a TikTok collection Credit cost: 1 (standard) Parameters: - url (required): Full URL of the TikTok collection. - max_cursor (optional, string): Cursor from the previous response to fetch the next page of collection videos. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. curl -G "https://www.socialcrawl.dev/v1/tiktok/collection/videos" \ --data-urlencode "url=https://www.tiktok.com/@kibblemaster808/collection/Want-to-go-7665668414573546258" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/profile/playlists List a TikTok account's playlists Credit cost: 1 (standard) Parameters: - handle (optional, string): TikTok username without the @ symbol. - user_id (optional, string): TikTok numeric user ID. Use this for faster responses. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - Constraint: one of handle, user_id (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/profile/playlists" \ --data-urlencode "handle=chipotle" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/playlist/videos List videos in a TikTok playlist Credit cost: 1 (standard) Parameters: - playlist_id (required): Playlist id from /v1/tiktok/profile/playlists (`post.id` on that response). - handle (optional, string): TikTok username of the playlist owner, without the @ symbol. - user_id (optional, string): TikTok numeric user ID of the playlist owner. Use this for faster responses. - cursor (optional, string): Cursor from the previous response to fetch the next page of videos. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - Constraint: one of handle, user_id (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/playlist/videos" \ --data-urlencode "playlist_id=7229371484127382314" \ --data-urlencode "handle=chipotle" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/user/liked List the videos a TikTok account has liked Credit cost: 1 (standard) Parameters: - handle (optional, string): TikTok username without the @ symbol. - user_id (optional, string): TikTok numeric user ID. Use this for faster responses. - cursor (optional, string): Cursor from the previous response to fetch the next page of liked videos. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. - Constraint: one of handle, user_id (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/user/liked" \ --data-urlencode "handle=chipotle" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/location/posts List TikTok videos tagged at a place Credit cost: 1 (standard) Parameters: - location_id (required): TikTok place id, as it appears in the place page URL. - cursor (optional, string): Cursor from the previous response to fetch the next page of videos. - region (optional, string): Two-letter country code for the proxy location. Defaults to US. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. curl -G "https://www.socialcrawl.dev/v1/tiktok/location/posts" \ --data-urlencode "location_id=22535865202815278" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/effects Get TikTok effects by id Credit cost: 1 (standard) Parameters: - ids (required): Comma-separated TikTok effect ids, up to 50 per request. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. curl -G "https://www.socialcrawl.dev/v1/tiktok/effects" \ --data-urlencode "ids=1108584,1109028" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/effect/videos List TikTok videos made with an effect Credit cost: 1 (standard) Parameters: - effect_id (required): TikTok effect id, as returned by /v1/tiktok/effects. - cursor (optional, string): Cursor from the previous response to fetch the next page of videos. - region (optional, string): Two-letter country code for the proxy location. Defaults to US. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. curl -G "https://www.socialcrawl.dev/v1/tiktok/effect/videos" \ --data-urlencode "effect_id=1108584" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/search/music Search TikTok sounds Credit cost: 1 (standard) Parameters: - query (required): Search keyword, matched against sound titles and creators. - cursor (optional, string): Cursor from the previous response to fetch the next page of sounds. - sort_by (optional, enum: relevance | most-used | most-recent | shortest | longest): Result ordering: relevance (default), most-used, most-recent, shortest or longest. - filter_by (optional, enum: all | title | creators): Which field the keyword is matched against: all (default), title or creators. - region (optional, string): Two-letter country code for the proxy location. Defaults to US. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. - max_pages (optional, integer): 1 to 50 (default 1, or 50 when `max_items` or `max_credits` is sent). Walk up to this many pages in one call and return the rows from all of them. Each page walked is billed exactly as one call to this endpoint (a cached page is free) and counts against your rate limit; its charge and any refund are recorded under the page's own request id, listed in `data.walk.request_ids`. The walk stops early at the last page, or when the request's time budget runs low; `data.walk.stopped` says why (`end`, `max_pages`, `max_items`, `max_credits`, `time_budget`, `page_error`) and `pagination.next_cursor` continues from where it stopped. - max_items (optional, integer): 1 to 5000. Get more in one call: keep walking pages until the response holds at least this many rows, then stop with `data.walk.stopped: "max_items"`. The last page is returned whole (it was billed), so `data.items` can hold up to one page more than this, and `pagination.next_cursor` resumes right after it without skipping a row. Walks up to 50 pages unless `max_pages` says fewer; each page is billed as one call (a cached page is free). Add `max_credits` to cap the spend. A long walk can stop on the time budget first (`"time_budget"`): send the returned cursor with the same params to continue. - max_credits (optional, integer): 1 to 100000. The most this call may spend, across every page it walks. A page starts only while the credits already spent plus that page's price (or, when higher, the most any page of this walk has cost) still fit, so the call never goes past it; the walk then stops with `data.walk.stopped: "max_credits"` and the cursor to resume. Sent alone, it walks as far as the budget goes (up to 50 pages). A value below the price of one page is a free 400 whose `error.details.page_credits` names that price. Credits a row join (`include=`) adds are known only once a page has run, so page 1 is checked against its own price. curl -G "https://www.socialcrawl.dev/v1/tiktok/search/music" \ --data-urlencode "query=espresso" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/hashtag Get TikTok hashtag details Credit cost: 1 (standard) Parameters: - hashtag (optional, string): Hashtag name, with or without the leading # symbol. - hashtag_id (optional, string): TikTok numeric hashtag id. Skips the name lookup when you already have it. - Constraint: one of hashtag, hashtag_id (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/hashtag" \ --data-urlencode "hashtag=kpop" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/ads/top Read TikTok's Creative Center Top Ads board: the best-performing ads in a market, ranked by CTR, impressions or engagement. Metered: 1 credit per ad returned, minimum 10 Credit cost: 10-100 (metered): 1 credit per ad returned, minimum 10 credits. A limit of 20 holds 20 credits and settles down to the number of ads that actually came back; a filter combination with no board costs nothing. With include=retention, 2 credits per ad that carries a retention curve (minimum 20), and 1 per ad TikTok serves no curve for. Parameters: - country (optional, enum: US | CA | MX | BR | GB | DE | FR | IT | ES | NL | PL | SE | TR | SA | AE | AU | JP | KR | ID | TH | VN | MY | PH | SG): Creative Center market to read the board for. Defaults to US. This is the market the ads ran in, not the language they are in. - period (optional, enum: 7 | 30 | 180): Lookback window in days: 7, 30 or 180. Defaults to 30. A longer window is a different board, not more rows. - order_by (optional, enum: for_you | impression | ctr | play_2s_rate | play_6s_rate | cvr | like): How TikTok ranks the board: for_you (TikTok's own blend, the default), ctr, impression, like, cvr, play_2s_rate or play_6s_rate. - ad_format (optional, enum: All ad types | Spark ads | Non-Spark ads): Restrict to Spark ads (ads boosted from an organic post) or Non-Spark ads. Defaults to all. - ad_language (optional, enum: en | ja | zh | vi | th | pt | id): Filter by the language of the ad copy. - like_tier (optional, enum: 1 | 2 | 3 | 4 | 5): TikTok's engagement percentile band, 1 (top) to 5. Use it to read further down the ranking without raising limit. - industry (optional, string): Creative Center sub-category label, for example 'Skincare' or 'Cosmetics'. Use a sub-category, not a top-level group: the board returns nothing for the broad groups such as 'Financial Services' or 'Education'. Omit for all industries. - keyword (optional, string): Search the board by brand or product keyword. - limit (optional, integer): How many ads to return, 1-100, default 20. The board is served in blocks of ten, so any limit below 10 still costs 10 credits. With include=retention, 1-20. - include (optional, string): Send `retention` to add each ad's second-by-second audience retention curve (`ext.ad.retention`) and its points at second 2 and second 6 (`ext.ad.retention_2s`, `ext.ad.retention_6s`). 2 credits per ad that carries a curve, minimum 20; up to 20 ads per call; usually 20 to 35 seconds; not combinable with keyword or industry. - since (optional, string): Only rows published on or after this date: YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp. Rows older than it are left off each page. This list is not in date order, so the walk does not stop at the date: keep following `next_cursor`, and each page costs what a page costs even when every row on it is filtered out. A row with no date is kept. `published_after`, `start_date`, `from_date`, `publish_time` (`yesterday`, `this-week`, `this-month`, `last-3-months`, `last-6-months`) and `max_age_days` are accepted and applied as `since`. - until (optional, string): Only rows published on or before this date: YYYY-MM-DD (that whole UTC day is included) or an ISO 8601 timestamp. Newer rows are left off each page and the walk does not stop, so older pages follow. A row with no date is kept. `published_before`, `end_date` and `to_date` are accepted and applied as `until`. Worth sending: - country=US: Results for one country. curl -G "https://www.socialcrawl.dev/v1/tiktok/ads/top" \ --data-urlencode "country=US" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/profile/full TikTok profile, recent posts, and computed analytics in one call. Credit cost: 5 (override; tier standard) Parameters: - handle (optional, string): TikTok username or handle, with or without a leading @. One of the identity params is required. - user_id (optional, string): Numeric TikTok user id. Use it instead of the handle when you already have the stable id, which survives a rename. One of the identity params is required. - posts (optional, integer): How many of the fetched recent posts to return and compute the metrics over (1-100, default 25). One call reads one page, which on TikTok is 10 videos, so a larger value returns that page and a `_warnings` note; pass posts_cursor back as cursor for the next page. - cursor (optional, string): Pass a prior response's posts_cursor to read the next page of posts. Each call is a separate 5-credit call whose metrics cover that page only. - include (optional, string): CSV subset of posts,computed (default both). include=computed omits the raw posts[] array from the response to save payload. The metrics are computed from the same fetched posts either way, so they do not change. - Constraint: one of handle, user_id (at least one required) curl -G "https://www.socialcrawl.dev/v1/tiktok/profile/full" \ --data-urlencode "handle=mrbeast" \ -H "x-api-key: YOUR_API_KEY" ## GET /v1/tiktok/search/mentions Search TikTok videos that mention an account Credit cost: 0-2 (metered): Metered, 0-2 credits: each call reads one page of /v1/tiktok/search (1 credit) and /v1/tiktok/search (1 credit), each billed at that endpoint's own price exactly as a direct call is. A page that found nothing or failed costs nothing, and a page served from cache is free. A page is billed when it returned rows, even if none of them carried the mention; data.legs shows each page's charge and data.excluded what was left out. Parameters: - handle (required): The account's username, with or without the @, or its profile URL. - cursor (optional, string): data.next_cursor from the previous response, sent back verbatim for the next page. - since (optional, string): Only posts published on or after this date, YYYY-MM-DD (UTC). - until (optional, string): Only posts published on or before this date, YYYY-MM-DD (UTC); that whole day is included. - include_self (optional, boolean): When true, also keep posts by the account itself. Default false. curl -G "https://www.socialcrawl.dev/v1/tiktok/search/mentions" \ --data-urlencode "handle=nike" \ -H "x-api-key: YOUR_API_KEY"