DEV Community

Qovia
Qovia

Posted on Originally published at qovia.ai

Compare how a keyword is doing on TikTok, YouTube and X in Python

You have a product name, a campaign hashtag or a competitor, and someone asks: "Where are people actually talking about this, TikTok, YouTube or X?" Answering that usually means three different APIs, three auth schemes and three response shapes. In this tutorial we'll write one short Python script that searches all three for the same keyword, normalizes the posts into one table, and prints a side-by-side comparison: how many posts, how recent, how much reach, and the top posts per platform.

We'll use Qovia, a paid public-data API that exposes TikTok, YouTube and X search (plus about 20 other platforms) behind one key and one JSON envelope. Full disclosure: I work on Qovia. Everything below uses only the documented endpoints from the API reference.

What you need

  • Python 3.9+ and requests (pip install requests pandas)
  • A Qovia API key from the console. New accounts get 50 trial calls, which is plenty for this tutorial. Each search call below costs $0.001 after that, and empty results are not charged.
export QOVIA_KEY="sk-qovia-..."
Enter fullscreen mode Exit fullscreen mode

The three endpoints

Platform Endpoint Useful params
TikTok GET /v1/raw/tiktok/search keyword, sort (general / most_liked), publish_time (all, day, week, month, quarter, half_year), region (e.g. US), count (max 30), integer cursor
YouTube GET /v1/raw/youtube/search keyword, upload_date (hour, today, week, month, year; default month), string cursor
X GET /v1/raw/x/search keyword (X search syntax works, e.g. #hashtag), type (top / media), string cursor

All three return the same record shape inside data.items: id, platform, text, author (with nickname, followers), metrics, and extra (with published_at in UTC and url). Pagination is the same everywhere: pass back data.cursor, stop when data.has_more is false.

The metrics differ per platform, and it's worth knowing that up front:

  • TikTok search records carry plays, likes, comments, shares, saves.
  • YouTube search records carry plays (view count) only. Likes and comments are on the video detail endpoint, raw.youtube.video.
  • X search records carry views, likes, comments, shares (reposts), quotes, saves.

So "reach" (plays or views) is comparable across all three, and "engagement" only across TikTok and X unless you enrich YouTube.

A tiny client

Every Qovia response is either {"request_id", "data", "meta"} on success or {"error": {"code", "message", ...}} on failure. Rate limits are 60 requests per minute on a new account, and a 429 comes with Retry-After, so the client honours that.

import os, time, requests

BASE = "https://api.qovia.ai"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['QOVIA_KEY']}"

def call(path, **params):
    for attempt in range(4):
        r = SESSION.get(BASE + path, params=params, timeout=60)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        body = r.json()
        if "error" in body:
            code = body["error"]["code"]
            if code in ("upstream_busy", "upstream_timeout") and attempt < 3:
                time.sleep(2 * (attempt + 1))
                continue
            raise RuntimeError(f"{path}: {code}: {body['error']['message']}")
        return body["data"] or {}, body["meta"]
    raise RuntimeError(f"{path}: gave up after retries")
Enter fullscreen mode Exit fullscreen mode

Two details from the docs worth respecting: unknown query parameters are rejected with 400 invalid_params (so typos fail loudly instead of being ignored), and an empty result is still HTTP 200 with meta.empty: true, which is why we fall back to {}.

Paging through each platform

def search(path, pages=3, **params):
    items, cursor = [], None
    for _ in range(pages):
        p = dict(params)
        if cursor is not None:
            p["cursor"] = cursor
        data, meta = call(path, **p)
        items += data.get("items", [])
        if not data.get("has_more"):
            break
        cursor = data.get("cursor")
    # TikTok notes adjacent pages may repeat a few posts: dedupe by id
    return list({it["id"]: it for it in items}.values())

def collect(keyword, pages=3):
    return {
        "tiktok": search("/v1/raw/tiktok/search", pages, keyword=keyword,
                         publish_time="month", region="US", count=30),
        "youtube": search("/v1/raw/youtube/search", pages, keyword=keyword,
                          upload_date="month"),
        "x": search("/v1/raw/x/search", pages, keyword=keyword, type="top"),
    }
Enter fullscreen mode Exit fullscreen mode

Three pages per platform is nine calls, under one cent once the trial is used up.

One honest caveat about the time windows: TikTok and YouTube let you ask for "the last month", while X search returns X's "top" order with no date filter. We'll filter X by published_at ourselves after the fact so the comparison is fair.

Normalizing into one table

import pandas as pd

def to_rows(results):
    rows = []
    for platform, items in results.items():
        for it in items:
            m, ex, au = it.get("metrics") or {}, it.get("extra") or {}, it.get("author") or {}
            reach = m.get("plays") if platform != "x" else m.get("views")
            engagement = None
            if platform in ("tiktok", "x"):
                engagement = sum((m.get(k) or 0) for k in ("likes", "comments", "shares", "saves"))
            rows.append({
                "platform": platform,
                "id": it["id"],
                "published_at": pd.to_datetime(ex.get("published_at"), utc=True),
                "author": au.get("nickname"),
                "author_followers": au.get("followers"),
                "reach": reach,
                "engagement": engagement,
                "text": (ex.get("title") or it.get("text") or "").replace("\n", " ")[:90],
                "url": ex.get("url"),
            })
    return pd.DataFrame(rows)
Enter fullscreen mode Exit fullscreen mode

YouTube records put the video title in extra.title and title-plus-snippet in text, so we prefer the title. Note that YouTube's author.followers is null in search results; the channel endpoint has subscriber counts if you need them.

The comparison

def compare(keyword, days=30):
    df = to_rows(collect(keyword))
    cutoff = pd.Timestamp.now(tz="UTC") - pd.Timedelta(days=days)
    df = df[df["published_at"] >= cutoff]

    summary = df.groupby("platform").agg(
        posts=("id", "count"),
        median_reach=("reach", "median"),
        total_reach=("reach", "sum"),
        median_engagement=("engagement", "median"),
        newest=("published_at", "max"),
    )
    print(f"\n=== '{keyword}' over the last {days} days ===")
    print(summary.to_string())

    for platform, group in df.groupby("platform"):
        print(f"\nTop 3 on {platform} by reach:")
        for _, r in group.nlargest(3, "reach").iterrows():
            print(f"  {int(r.reach or 0):>10,}  {r.author}: {r.text}")
            print(f"              {r.url}")
    return df

if __name__ == "__main__":
    import sys
    df = compare(sys.argv[1] if len(sys.argv) > 1 else "cursor ai")
    df.to_csv("comparison.csv", index=False)
Enter fullscreen mode Exit fullscreen mode

Run it:

python compare.py "your product name"
Enter fullscreen mode Exit fullscreen mode

You get a summary table with one row per platform plus the three biggest posts on each, and a comparison.csv you can open in a spreadsheet or feed into a notebook.

Reading the result

A few patterns show up quickly when you run this on real keywords:

  • Lots of TikTok posts, low median plays: the topic is being used as a hashtag by small creators. Good for UGC, weak as a signal of reach.
  • Few YouTube videos, high median plays: long-form explainers and reviews own the topic. That's where search traffic goes.
  • X dominated by a few accounts: check author_followers. If the top posts are all from accounts with huge followings, the "conversation" is really a couple of announcements and their replies.

Because the three samples are each platform's own ranking (TikTok relevance, YouTube relevance, X "top"), treat counts as a sample, not a census. The useful signal is the relative shape: where reach concentrates, how fresh the newest posts are, and who is driving it.

Going further

  • Engagement for YouTube: call GET /v1/raw/youtube/video?video_id=... with extra.source_id for the top videos to get likes and comment counts.
  • What people actually say: pull comments with raw.tiktok.comments, raw.youtube.comments and raw.x.comments for the top posts and run the same sentiment or keyword pass across all three.
  • Run it daily: append each run's summary with a date column and you have a cross-platform trend line for free.
  • Ask an assistant instead: the same endpoints are available as an MCP server at https://api.qovia.ai/mcp (OAuth or the same API key as a Bearer token), so Claude, Cursor or VS Code can run this comparison conversationally. See the MCP docs.

The full parameter list for every endpoint is in the API reference, and there's a single-file version at qovia.ai/llms-full.txt if you want to drop it into a model's context.


This article was written with AI assistance and reviewed against Qovia's documentation. I work on Qovia.

Top comments (0)