Skip to main content

Overview

VidNavigator offers two ways to collect TikTok videos:

Profile Scrape

Walk a single creator’s account and return their videos, filtered by date and likes.

Keyword Search

Search across TikTok by keyword and return matching videos from many creators.
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.
These endpoints don’t have dedicated SDK helpers yet, so the examples below use raw HTTP (cURL, Python requests, and fetch).

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.
EndpointWhat 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 the pages. No search_request.
See 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.
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
  }'
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)
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);

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.
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"])
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`);
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.

Searching TikTok by Keyword

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.
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
  }'
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)
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);

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.
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"])
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);
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).
Use caseEndpoint
Collect one creator’s catalogProfile scrape
Track a hashtag, topic, or trend across creatorsKeyword search
Filter by date range and likesBoth (add after_datetime / before_datetime, min_likes / max_likes)
Filter by viewsKeyword search (min_views / max_views)
Maximize coverage of a keywordKeyword search with parallel_search_slices up to 4

Next Steps