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

> Source: <https://dev.to/qovia/compare-how-a-keyword-is-doing-on-tiktok-youtube-and-x-in-python-4nc3>
> Published: 2026-11-04 03:45:10+00:00

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](https://qovia.ai), 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](https://qovia.ai/docs/api-reference/).

`requests` (`pip install requests pandas`)

```
export QOVIA_KEY="sk-qovia-..."
```

| 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:

`plays`, `likes`, `comments`, `shares`, `saves`.` raw.youtube.video`.` views`, `quotes`, So "reach" (plays or views) is comparable across all three, and "engagement" only across TikTok and X unless you enrich YouTube.

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.

``` python
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")
```

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 `{}`.

``` python
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"),
    }
```

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.

``` python
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)
```

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.

``` python
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)
```

Run it:

```
python compare.py "your product name"
```

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.

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

`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.

`GET /v1/raw/youtube/video?video_id=...` with `extra.source_id` for the top videos to get likes and comment counts.`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.`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 The full parameter list for every endpoint is in the [API reference](https://qovia.ai/docs/api-reference/), and there's a single-file version at [qovia.ai/llms-full.txt](https://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.*
