I build Qovia, the API used in this post. The workflow itself (resolve a video, list the creator's top videos, compute engagement, collect comments, append to CSV) works with any Kuaishou data source; the CSV and pandas parts are plain Python.
Kuaishou (快手, Kwai abroad) is China's other big short-video app next to Douyin. Its audience leans more toward smaller cities and live commerce, and a lot of creators who matter for consumer brands there are barely visible on Douyin. If you're evaluating a Kuaishou creator for a campaign, or just want to know what's working in a niche, the questions are usually the same: how many plays do their best videos get, how engaged are viewers, what do they use as hashtags, and what do people say in the comments?
A note on the angle: the Kuaishou endpoints are built around a video link (its eid) or a creator's numeric uid, and from there you can walk to everything else. There's no Kuaishou-only keyword search; for finding a starting video by keyword, Qovia's cross-platform search can be limited to short-video apps (Step 2b). The hot board gives you a separate view of what's trending.
In this tutorial we'll write a script that:
- takes one Kuaishou video link and fetches its detail, including the creator's numeric uid,
- pulls the creator's hot videos (most liked first) and computes engagement stats,
- collects the first pages of comments under their top videos,
- snapshots the Kuaishou hot board,
- appends everything to CSV and summarizes it with pandas.
Setup
You need Python 3.9+, requests and pandas:
pip install requests pandas
export QOVIA_KEY=sk-qovia-... # key from the Qovia console
Get a key from Qovia. Requests authenticate with Authorization: Bearer <key> (an x-api-key header also works). The Kuaishou API page lists the five Kuaishou endpoints with a real request and response.
Step 1: a small request helper
Every response has the same envelope: the result is in data, and meta reports the charge (meta.billed, meta.charged_usd, meta.empty). Empty results come back as HTTP 200 with meta.empty: true and are not charged. Errors are {"error": {"code", "message", ...}} and are never charged. The video detail endpoint answers 404 for a post that doesn't exist or was deleted, so the helper turns that into None. A 429 carries a Retry-After header, so the helper waits and retries once:
import os
import time
import requests
BASE = "https://api.qovia.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['QOVIA_KEY']}"}
spent = 0.0
def get(path, **params):
global spent
params = {k: v for k, v in params.items() if v is not None}
for attempt in range(2):
r = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=60)
if r.status_code == 429 and attempt == 0:
time.sleep(int(r.headers.get("Retry-After", "10")))
continue
if r.status_code == 404:
return None # deleted or missing, not charged
r.raise_for_status()
body = r.json()
spent += float((body.get("meta") or {}).get("charged_usd") or 0)
return body.get("data") or {}
Unknown parameters are rejected with 400 invalid_params, which is why the helper drops None values instead of sending them.
Step 2: from a video link to the creator
Kuaishou video links look like https://www.kuaishou.com/short-video/3xry7mhn2ywjyqg. The last part is the video's eid, and that's what the API takes; the long numeric photo ID is not accepted.
GET /v1/raw/kuaishou/video with video_id=<eid> returns one post: text (the caption), author (nickname, numeric uid, eid, and unique_id, the Kuaishou ID), metrics (plays, likes, comments, shares, saves) and extra with eid, published_at (Beijing time), video_dur_s, topics (hashtags), cover_url, ip_location and url.
The important bit for us is author.uid: the creator endpoint takes the numeric uid, not the eid.
import re
EID_RE = re.compile(r"(3x[0-9a-z]{10,20})")
def eid_from_link(link):
m = EID_RE.search(link)
if not m:
raise ValueError(f"no Kuaishou eid in {link!r}")
return m.group(1)
def resolve(link):
video = get("/v1/raw/kuaishou/video", video_id=eid_from_link(link))
if not video:
raise SystemExit("video not found (deleted or private)")
a = video["author"]
print(f"{a.get('nickname')} (uid {a['uid']}, Kuaishou ID {a.get('unique_id')})")
return video
The 3x... pattern matches the format the API validates; if Kuaishou changes its link format, you can always paste the eid directly.
Step 2b (optional): find a starting video by keyword
If you don't have a link yet, POST /v1/search searches public content across Chinese platforms. With platforms: ["short_video"] (and types: ["original"] to skip reposts and comments) it is limited to "Kuaishou and other short-video apps", so the results can include other apps too; keep the ones whose url is a Kuaishou link. Dates are whole days in Beijing time (the last 7 days by default), 10 or 20 results per page. This endpoint is priced differently from the Kuaishou ones (see Cost).
def find_kuaishou_links(query, limit=20):
global spent
r = requests.post(f"{BASE}/v1/search", headers=HEADERS, timeout=60,
json={"query": query, "platforms": ["short_video"],
"types": ["original"], "limit": limit})
r.raise_for_status()
body = r.json()
spent += float((body.get("meta") or {}).get("charged_usd") or 0)
items = (body.get("data") or {}).get("items", [])
return [i["url"] for i in items
if "kuaishou.com" in (i.get("url") or "") and EID_RE.search(i["url"])]
I haven't checked what share of short_video results are Kuaishou for a given query, so treat this as a way to find candidates, then run the rest of the pipeline on a link you picked.
Step 3: the creator's hot videos and their stats
GET /v1/raw/kuaishou/creator_hot_videos takes the uid and returns the creator's hot posts, most liked first, 20 per page, as the same records as the video endpoint. Pass cursor back for the next page and stop when has_more is false.
This is the creator's hot list, not a full upload history, so treat the numbers as "what their best content does" rather than an average over everything they post. That's still the number most people want when comparing creators.
import csv
from datetime import datetime, timezone
from pathlib import Path
VIDEO_FIELDS = ["checked_at", "creator_uid", "creator", "eid", "caption", "plays",
"likes", "comments", "shares", "saves", "engagement_rate",
"duration_s", "topics", "published_at", "url"]
def now():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def hot_videos(uid, max_pages=2):
videos, seen, cursor = [], set(), None
for _ in range(max_pages):
page = get("/v1/raw/kuaishou/creator_hot_videos", uid=uid, cursor=cursor)
for v in (page or {}).get("items", []):
if v["extra"]["eid"] not in seen:
seen.add(v["extra"]["eid"])
videos.append(v)
if not (page or {}).get("has_more"):
break
cursor = page.get("cursor")
return videos
def video_row(v):
m, ex, a = v.get("metrics") or {}, v.get("extra") or {}, v.get("author") or {}
plays = m.get("plays") or 0
actions = sum(m.get(k) or 0 for k in ("likes", "comments", "shares"))
return {
"checked_at": now(),
"creator_uid": a.get("uid"),
"creator": a.get("nickname"),
"eid": ex.get("eid"),
"caption": (v.get("text") or "").replace("\n", " ")[:200],
"plays": m.get("plays"),
"likes": m.get("likes"),
"comments": m.get("comments"),
"shares": m.get("shares"),
"saves": m.get("saves"),
"engagement_rate": round(actions / plays, 4) if plays else None,
"duration_s": ex.get("video_dur_s"),
"topics": "|".join(ex.get("topics") or []),
"published_at": ex.get("published_at"),
"url": ex.get("url"),
}
def append_csv(path, fields, rows):
path = Path(path)
new_file = not path.exists()
with path.open("a", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=fields)
if new_file:
w.writeheader()
w.writerows(rows)
engagement_rate here is (likes + comments + shares) / plays. That's my definition, not Kuaishou's; pick whatever ratio fits your use, but keep it consistent across creators.
Step 4: what viewers say
GET /v1/raw/kuaishou/comments takes the same video_id (eid) and returns first-level comments with a cursor: about 30 on the first page, then about 20. Each comment has cid, text, likes, reply_count, ip_location, created_at (Beijing time), is_hot (Kuaishou's hot-comment flag), liked_by_author, images and author; the page also has total, cursor and has_more.
liked_by_author is a nice signal for creator research: it tells you which comments the creator chose to like, which often says something about how they manage their audience.
COMMENT_FIELDS = ["checked_at", "eid", "cid", "text", "likes", "reply_count",
"ip_location", "created_at", "is_hot", "liked_by_author"]
def comment_rows(videos, top=5, pages_per_video=1):
rows = []
for v in videos[:top]:
eid, cursor = v["extra"]["eid"], None
for _ in range(pages_per_video):
page = get("/v1/raw/kuaishou/comments", video_id=eid, cursor=cursor)
for c in (page or {}).get("items", []):
rows.append({
"checked_at": now(),
"eid": eid,
"cid": c["cid"],
"text": (c.get("text") or "").replace("\n", " "),
"likes": c.get("likes"),
"reply_count": c.get("reply_count"),
"ip_location": c.get("ip_location"),
"created_at": c.get("created_at"),
"is_hot": c.get("is_hot"),
"liked_by_author": c.get("liked_by_author"),
})
if not (page or {}).get("has_more"):
break
cursor = page.get("cursor")
return rows
Since creator_hot_videos is already sorted most liked first, videos[:top] is the creator's top five.
For a comment with a big reply_count, GET /v1/raw/kuaishou/comment_replies with video_id and comment_id=<cid> returns the thread (about 12 per page, same cursor rules). Each reply has parent_cid and reply_to (the user being answered). Kuaishou doesn't report a reply total, so just page until has_more is false. I left it out of the main loop to keep the call count predictable.
Step 5: what's trending right now
GET /v1/raw/kuaishou/hot_board takes no parameters and returns the whole main hot board in one call, about 50 entries. Each has rank, keyword, hot_value, views, label (the board's tag, such as 霸榜中 "holding the top"), pinned, word_id and a search url. Pinned entries come first and have no rank.
BOARD_FIELDS = ["checked_at", "rank", "keyword", "hot_value", "views", "label", "pinned"]
def board_rows():
board = get("/v1/raw/kuaishou/hot_board") or {}
ts = now()
return [{"checked_at": ts, **{k: w.get(k) for k in BOARD_FIELDS[1:]}}
for w in board.get("items", [])]
Snapshot it every hour or so and you have a history of what Kuaishou users were looking at, which is handy context next to a creator's numbers.
Step 6: put it together
import sys
def run(link):
video = resolve(link)
uid = video["author"]["uid"]
videos = hot_videos(uid, max_pages=2)
append_csv("ks_videos.csv", VIDEO_FIELDS, [video_row(v) for v in videos])
comments = comment_rows(videos, top=5, pages_per_video=1)
append_csv("ks_comments.csv", COMMENT_FIELDS, comments)
append_csv("ks_hot_board.csv", BOARD_FIELDS, board_rows())
print(f"{len(videos)} hot videos, {len(comments)} comments")
print(f"spent this run: ${spent:.4f}")
if __name__ == "__main__":
run(sys.argv[1])
python3 ks_creator.py https://www.kuaishou.com/short-video/3xry7mhn2ywjyqg
Output looks something like this (illustrative, not real data):
权少爱吃小汉堡🍔 (uid 228905802, Kuaishou ID quanshaogmod)
40 hot videos, 150 comments
spent this run: $0.0090
Step 7: summarize with pandas
import pandas as pd
videos = pd.read_csv("ks_videos.csv", dtype={"creator_uid": str, "eid": str})
comments = pd.read_csv("ks_comments.csv", dtype={"eid": str, "cid": str})
v = videos.sort_values("checked_at").drop_duplicates("eid", keep="last")
for uid, g in v.groupby("creator_uid"):
print(f"\n{g['creator'].iloc[0]} ({len(g)} hot videos)")
print(f" median plays: {g['plays'].median():,.0f}")
print(f" median likes: {g['likes'].median():,.0f}")
print(f" median engagement: {g['engagement_rate'].median():.2%}")
print(f" median duration (s): {g['duration_s'].median():.0f}")
print(" top hashtags:", g["topics"].dropna().str.split("|").explode().value_counts().head(5).to_dict())
print("\nTop videos by plays:")
print(v.sort_values("plays", ascending=False)[["plays", "likes", "engagement_rate", "caption", "url"]].head(10))
c = comments.drop_duplicates("cid", keep="last")
print("\nWhere commenters are (IP location):")
print(c["ip_location"].value_counts().head(10))
print(f"\nHot comments: {c['is_hot'].fillna(False).astype(bool).mean():.0%}, "
f"liked by the creator: {c['liked_by_author'].fillna(False).astype(bool).mean():.0%}")
print("\nMost liked comments:")
print(c.sort_values("likes", ascending=False)[["likes", "text"]].head(10))
To compare several creators, run the script once per creator (one video link each); the rows accumulate in the same CSV and the groupby prints one block per creator. Because every row has a checked_at timestamp, running it weekly also lets you see whether a creator's top videos are still gaining plays.
Doing the same from Claude or Cursor
If you'd rather ask questions than maintain a script, the same endpoints are available through Qovia's MCP server at https://api.qovia.ai/mcp (Streamable HTTP, same API key in an Authorization: Bearer header, same billing as the HTTP API). In Claude Code:
claude mcp add --transport http qovia https://api.qovia.ai/mcp \
--header "Authorization: Bearer $QOVIA_KEY"
Then ask something like "for the creator of https://www.kuaishou.com/short-video/3xry7mhn2ywjyqg, list their ten most liked videos with plays and engagement, and summarize the top comments on the first three." Configs for Cursor, VS Code and Claude Desktop are in the MCP docs, and the Kuaishou MCP page lists the Kuaishou tools. The assistant is good for one-off digging; the script is better when you want the same numbers every week.
Cost
Every Kuaishou endpoint used here (video, creator_hot_videos, comments, comment_replies, hot_board) is a fixed $0.001 per successful call ($1 per 1,000). The optional keyword search in Step 2b (POST /v1/search) is $0.005 per call ($5 per 1,000). Empty results, errors and the 404 for a missing video are not charged.
As a rough example (arithmetic only): one run above is 1 video detail, up to 2 hot-video pages, 5 comment pages and 1 hot board, so at most 9 calls, about $0.009. Checking 20 creators a week is well under $1 a month. The spent counter prints the real number each run. New accounts get 50 trial calls, counted only when a call returns data; after that it's a prepaid balance with no monthly fee. Details are on the pricing page, and every parameter is in the API docs.
The default rate limit is 60 requests per minute per account, more than enough for a job like this.
Limits
- Public content only: videos, comments and the hot board.
- The Kuaishou endpoints are marked beta.
- No Kuaishou-only keyword search: you start from a video link (eid) or a creator's numeric uid. The cross-platform search in Step 2b covers Kuaishou together with other short-video apps.
-
creator_hot_videosis the creator's hot list, most liked first, not their full upload history, so stats skew toward their best videos. -
author.followerscan be null on video records; don't build on follower counts from these endpoints. - The video and comment endpoints take the eid (
3x...), the creator endpoint takes the numericuid. Mixing them up gets you a400. - Counts are a snapshot at the time of the call. Compare snapshots across runs rather than treating one as final.
If you build on this, tell me in the comments what creator metrics you'd want added.
This article was written with the help of AI.
Top comments (0)