cd /news/ai-tools/compare-how-a-keyword-is-doing-on-ti… · home › topics › ai-tools › article
[ARTICLE · art-163577] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

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

A developer at Qovia published a Python walkthrough for comparing keyword activity across TikTok, YouTube and X using the company's unified public-data API, which exposes search across roughly 20 platforms behind a single key and JSON envelope. The script normalizes posts into one table and reports post counts, recency, reach and top posts per platform, handling pagination, 429 rate limits via Retry-After, and platform-specific metric differences. The author notes reach (plays or views) is comparable across all three platforms, while engagement is only comparable between TikTok and X without enriching YouTube data.

by read5 min views2 publishedNov 4, 2026

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.

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), integercursor
YouTube GET /v1/raw/youtube/search keyword ,upload_date (hour ,today ,week ,month ,year ; defaultmonth ), stringcursor
X GET /v1/raw/x/search keyword (X search syntax works, e.g.#hashtag ),type (top /media ), stringcursor

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.

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

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")
    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.

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.

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

── more in #ai-tools 4 stories · sorted by recency
── more on @qovia 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/compare-how-a-keywor…] indexed:0 read:5min 2026-11-04 · —