> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vidnavigator.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Searching TikTok

> Collect TikTok videos from a profile or by keyword using the asynchronous TikTok endpoints, then poll for paginated results.

## Overview

VidNavigator offers two ways to collect TikTok videos:

<CardGroup cols={2}>
  <Card title="Profile Scrape" icon="user" href="/api-reference/endpoint/tiktok-profile">
    Walk a single creator's account and return their videos, filtered by date and likes.
  </Card>

  <Card title="Keyword Search" icon="magnifying-glass" href="/api-reference/endpoint/tiktok-search">
    Search across TikTok by keyword and return matching videos from many creators.
  </Card>
</CardGroup>

Both are **asynchronous**: you submit a task, get a `task_id` back immediately, then poll a `GET` endpoint until the task is `completed`. Results are retained for about 1 hour, and large result sets can be downloaded as a single JSON file via a temporary `download_url`.

<Note>
  These endpoints don't have dedicated SDK helpers yet, so the examples below use raw HTTP (cURL, Python `requests`, and `fetch`).
</Note>

## Prerequisites

* A valid VidNavigator API key.

## Billing

Both endpoints bill in the background worker, so **usage is disclosed on the polling `GET`** (pass `include_usage=true` there), not at submit time.

| Endpoint                           | What you're billed                                                                                                                                       |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Profile scrape (`/tiktok/profile`) | `standard_request` per TikTok page fetched (`residential_request` when a residential proxy is used).                                                     |
| Keyword search (`/tiktok/search`)  | `residential_request` per page fetched (`pages_fetched × residential_request`). An N-slice search bills up to **N×** the pages. **No `search_request`.** |

See [Usage & Costs](/api-reference/usage-costs) for credit conversions.

## Searching a TikTok Profile

### 1. Submit the scrape

Send a public `profile_url` and optional filters. `max_posts` and `after_datetime` let the scraper stop paginating early.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.vidnavigator.com/v1/tiktok/profile" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "profile_url": "https://www.tiktok.com/@tiktok",
      "max_posts": 50,
      "after_datetime": "2024-01-01T00:00:00Z",
      "min_likes": 10000
    }'
  ```

  ```python Python theme={null}
  import requests

  BASE = "https://api.vidnavigator.com/v1"
  HEADERS = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}

  resp = requests.post(
      f"{BASE}/tiktok/profile",
      headers=HEADERS,
      json={
          "profile_url": "https://www.tiktok.com/@tiktok",
          "max_posts": 50,
          "after_datetime": "2024-01-01T00:00:00Z",
          "min_likes": 10000,
      },
  )
  task_id = resp.json()["data"]["task_id"]
  print("Task ID:", task_id)
  ```

  ```javascript JavaScript theme={null}
  const BASE = "https://api.vidnavigator.com/v1";
  const HEADERS = {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  };

  const submit = await fetch(`${BASE}/tiktok/profile`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      profile_url: "https://www.tiktok.com/@tiktok",
      max_posts: 50,
      after_datetime: "2024-01-01T00:00:00Z",
      min_likes: 10000,
    }),
  });
  const { data } = await submit.json();
  const taskId = data.task_id;
  console.log("Task ID:", taskId);
  ```
</CodeGroup>

### 2. Poll until completed and page through results

Poll `GET /tiktok/profile/{task_id}` while `task_status` is `processing`. Once `completed`, walk the pages using the `next_cursor` from `data.pagination`.

<CodeGroup>
  ```python Python theme={null}
  import time

  def get_profile_page(task_id, cursor=None, limit=50):
      params = {"limit": limit}
      if cursor:
          params["cursor"] = cursor
      r = requests.get(f"{BASE}/tiktok/profile/{task_id}", headers=HEADERS, params=params)
      return r.json()["data"]

  # Wait for completion
  while True:
      data = get_profile_page(task_id, limit=1)
      if data["task_status"] != "processing":
          break
      time.sleep(3)

  if data["task_status"] == "failed":
      raise RuntimeError(data.get("error_message", "scrape failed"))

  # Page through all videos
  videos, cursor = [], None
  while True:
      page = get_profile_page(task_id, cursor=cursor, limit=100)
      videos.extend(page["videos"])
      pagination = page["pagination"]
      if not pagination["has_next"]:
          break
      cursor = pagination["next_cursor"]

  print(f"Collected {len(videos)} videos")
  for v in videos[:5]:
      print(v["published_at"], v["likes"], v["url"])
  ```

  ```javascript JavaScript theme={null}
  async function getProfilePage(taskId, { cursor, limit = 50, includeUsage = false } = {}) {
    const params = new URLSearchParams({ limit: String(limit) });
    if (cursor) params.set("cursor", cursor);
    if (includeUsage) params.set("include_usage", "true");
    const r = await fetch(`${BASE}/tiktok/profile/${taskId}?${params}`, { headers: HEADERS });
    return (await r.json()).data;
  }

  // Wait for completion
  let status;
  do {
    status = await getProfilePage(taskId, { limit: 1 });
    if (status.task_status === "processing") await new Promise((res) => setTimeout(res, 3000));
  } while (status.task_status === "processing");

  if (status.task_status === "failed") throw new Error(status.error_message || "scrape failed");

  // Page through all videos
  const videos = [];
  let cursor = null;
  let hasNext = true;
  while (hasNext) {
    const page = await getProfilePage(taskId, { cursor, limit: 100 });
    videos.push(...page.videos);
    hasNext = page.pagination.has_next;
    cursor = page.pagination.next_cursor;
  }

  console.log(`Collected ${videos.length} videos`);
  ```
</CodeGroup>

<Tip>
  For very large profiles, skip pagination and download the whole result at once: once the task is `completed`, `data.download_url` holds a short-lived signed URL to the full JSON. Call the `GET` endpoint again to mint a fresh URL if it expires.
</Tip>

## Searching TikTok by Keyword

### 1. Submit the search

Keyword search returns videos across many creators, sorted by `published_at` (newest first). Use `parallel_search_slices` (1–4) to lift the result ceiling, and `after_datetime` / `before_datetime` for time windows.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.vidnavigator.com/v1/tiktok/search" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "ai tools",
      "parallel_search_slices": 2,
      "after_datetime": "2024-01-01T00:00:00Z",
      "min_likes": 1000
    }'
  ```

  ```python Python theme={null}
  resp = requests.post(
      f"{BASE}/tiktok/search",
      headers=HEADERS,
      json={
          "query": "ai tools",
          "parallel_search_slices": 2,
          "after_datetime": "2024-01-01T00:00:00Z",
          "min_likes": 1000,
      },
  )
  task_id = resp.json()["data"]["task_id"]
  print("Task ID:", task_id)
  ```

  ```javascript JavaScript theme={null}
  const submit = await fetch(`${BASE}/tiktok/search`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      query: "ai tools",
      parallel_search_slices: 2,
      after_datetime: "2024-01-01T00:00:00Z",
      min_likes: 1000,
    }),
  });
  const taskId = (await submit.json()).data.task_id;
  console.log("Task ID:", taskId);
  ```
</CodeGroup>

### 2. Poll, paginate, and read usage

The polling flow is identical to the profile scrape — just swap the path to `/tiktok/search/{task_id}` and read `data.results` instead of `data.videos`. Pass `include_usage=true` once the task is `completed` to see what was billed.

<CodeGroup>
  ```python Python theme={null}
  import time

  def get_search_page(task_id, cursor=None, limit=50, include_usage=False):
      params = {"limit": limit}
      if cursor:
          params["cursor"] = cursor
      if include_usage:
          params["include_usage"] = "true"
      r = requests.get(f"{BASE}/tiktok/search/{task_id}", headers=HEADERS, params=params)
      return r.json()

  # Wait for completion
  while True:
      body = get_search_page(task_id, limit=1)
      if body["data"]["task_status"] != "processing":
          break
      time.sleep(3)

  # First completed page, with usage disclosure
  body = get_search_page(task_id, limit=50, include_usage=True)
  data = body["data"]
  print(f"{data['stats']['results_count']} results across {data['stats']['pages_fetched']} pages")

  for item in data["results"][:5]:
      print(item["published_at"], item["stats"]["likes"], item["url"])

  # Usage is at the top level of the response
  if "usage" in body:
      print("Credits charged:", body["usage"]["total_credits"])
  ```

  ```javascript JavaScript theme={null}
  async function getSearchPage(taskId, { cursor, limit = 50, includeUsage = false } = {}) {
    const params = new URLSearchParams({ limit: String(limit) });
    if (cursor) params.set("cursor", cursor);
    if (includeUsage) params.set("include_usage", "true");
    const r = await fetch(`${BASE}/tiktok/search/${taskId}?${params}`, { headers: HEADERS });
    return await r.json();
  }

  // Wait for completion
  let body;
  do {
    body = await getSearchPage(taskId, { limit: 1 });
    if (body.data.task_status === "processing") await new Promise((res) => setTimeout(res, 3000));
  } while (body.data.task_status === "processing");

  // First completed page with usage disclosure
  body = await getSearchPage(taskId, { limit: 50, includeUsage: true });
  const { results, stats } = body.data;
  console.log(`${stats.results_count} results across ${stats.pages_fetched} pages`);
  results.slice(0, 5).forEach((item) =>
    console.log(item.published_at, item.stats.likes, item.url)
  );
  if (body.usage) console.log("Credits charged:", body.usage.total_credits);
  ```
</CodeGroup>

<Warning>
  Don't pass `include_usage` on the submit (`POST`) call — it's ignored because billing happens after the `202` response. Pass it on the polling `GET`, and only expect a `usage` block once `task_status=completed` (failed tasks have all charges refunded).
</Warning>

## Choosing Between Profile and Keyword Search

| Use case                                         | Endpoint                                                                   |
| ------------------------------------------------ | -------------------------------------------------------------------------- |
| Collect one creator's catalog                    | Profile scrape                                                             |
| Track a hashtag, topic, or trend across creators | Keyword search                                                             |
| Filter by date range and likes                   | Both (add `after_datetime` / `before_datetime`, `min_likes` / `max_likes`) |
| Filter by views                                  | Keyword search (`min_views` / `max_views`)                                 |
| Maximize coverage of a keyword                   | Keyword search with `parallel_search_slices` up to `4`                     |

## Next Steps

* [TikTok Profile Scrape reference](/api-reference/endpoint/tiktok-profile)
* [TikTok Keyword Search reference](/api-reference/endpoint/tiktok-search)
* [Usage & Costs](/api-reference/usage-costs)
