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