Weibo API in Python: search, profiles and hot search (2026)
Three endpoints: Weibo post search, Weibo account profile by UID and Weibo hot search, each a plain GET with an API key, no OAuth app and no login cookie. The worked example below is 9 calls and costs $0.018; a failed call is not charged.
Updated
Weibo does have an open API, but it is built around the user who authorizes your app. The interface list on open.weibo.com runs through OAuth 2.0: read the authorized user's profile, their own posts, their home timeline, the comments on a post, and a few write calls such as sharing a link. Interfaces marked as advanced need a separate application. Keyword search is not in the standard list. The closest thing, search/topics, is an advanced interface that only searches posts inside a #topic# (the keyword has to be a topic between two # signs) and returns only the newest 200 results. Wider access goes through Weibo's commercial data interfaces, which come with their own subscription terms.
So most Python scripts for Weibo data scrape the site with a logged-in cookie, as searching Weibo without a cookie describes. This guide skips both the OAuth app and the cookie: each call is an HTTPS GET with an API key and requests is the only library. You end with a CSV of posts, a CSV of the accounts that posted them, and a check against the hot search board.
Step by step
Set the key once: export OKDATA_API_KEY=okd_.... Every answer has code ("ok" on success); lists come in data.items with a page, single objects in data.item.
Step 1: search posts by keyword
Weibo post search takes query, typed as you would in Weibo's search box, and sort: hot (the default, most engagement) or latest (newest first). One call returns about 10 posts for $0.002. Each has id, url, text, published_at, images, hashtags, and metrics.likes, metrics.comments and metrics.reposts; video posts also carry a play count in metrics.views. The author comes with the UID in author.id, author.name, author.followers and author.verified.
curl -G "https://api.okdata.online/v1/weibo/posts/search" -H "Authorization: Bearer $OKDATA_API_KEY" \
--data-urlencode 'query=预制菜' --data-urlencode 'sort=latest'
For page two and on, send page.next_cursor back as cursor, unchanged; stop when page.has_more is false. With view=full, ext.region gives the place Weibo shows as 发布于, ext.source the client it was posted from, and ext.is_long_text flags posts that search shows cut short.
Step 2: look up the authors
Weibo account profile takes the numeric UID as id, the same number as author.id in search or the digits in weibo.com/u/5730129846. For $0.002 it returns name, bio, followers, following, posts and verified; with view=full, ext adds verified_reason, ip_location, location and gender. Looking up the UID behind a custom profile address is covered in getting a profile from a Weibo UID.
curl -G "https://api.okdata.online/v1/weibo/users/get" -H "Authorization: Bearer $OKDATA_API_KEY" \
--data-urlencode 'id=5730129846' --data-urlencode 'view=full'
Step 3: read the hot search board
Weibo hot search takes no parameters. One call returns the current board, about 50 rows, for $0.002: rank, name (the search term), heat and url (the search page for that topic). Paid placements are left out.
curl -G "https://api.okdata.online/v1/weibo/trends/list" -H "Authorization: Bearer $OKDATA_API_KEY"
Step 4: the full script
The helper checks body["code"], returns None for deleted or banned accounts, and retries rate limits and busy upstreams with the same Idempotency-Key, so a retry is never charged twice. MAX_PAGES caps the search.
import csv
import os
import time
import uuid
import requests
API = "https://api.okdata.online"
KEY = os.environ["OKDATA_API_KEY"]
QUERY = "预制菜"
MAX_PAGES = 3 # search pages; each page is one call
TOP_AUTHORS = 5 # profile lookups
def call(path, **params):
headers = {"Authorization": f"Bearer {KEY}", "Idempotency-Key": str(uuid.uuid4())}
for attempt in range(4):
body = requests.get(API + path, headers=headers, params=params, timeout=60).json()
if body["code"] == "ok":
return body
if body["code"] == "not_found":
return None # deleted, banned or private: not charged
if body.get("retry") in ("yes", "same_key"):
time.sleep(body.get("retry_after_seconds") or 2 ** attempt)
continue
raise RuntimeError(f"{body['code']}: {body['message']} (request {body['request_id']})")
raise RuntimeError(f"{path}: still failing after 4 tries")
def search(query, max_pages):
cursor = None
for _ in range(max_pages):
body = call("/v1/weibo/posts/search", query=query, sort="latest", view="full",
**({"cursor": cursor} if cursor else {}))
yield from body["data"]["items"]
if not body["page"]["has_more"]:
return
cursor = body["page"]["next_cursor"]
posts = list(search(QUERY, MAX_PAGES))
with open("weibo_posts.csv", "w", newline="", encoding="utf-8-sig") as f:
out = csv.writer(f)
out.writerow(["post_id", "published_at", "uid", "author", "text", "likes", "comments", "reposts", "region", "url"])
for p in posts:
m = p["metrics"]
out.writerow([p["id"], p["published_at"], p["author"]["id"], p["author"]["name"], p["text"],
m["likes"], m["comments"], m["reposts"], p["ext"].get("region"), p["url"]])
authors = {p["author"]["id"]: p["author"] for p in posts}
biggest = sorted(authors.values(), key=lambda a: a["followers"] or 0, reverse=True)[:TOP_AUTHORS]
with open("weibo_authors.csv", "w", newline="", encoding="utf-8-sig") as f:
out = csv.writer(f)
out.writerow(["uid", "name", "followers", "following", "posts", "verified", "verified_reason", "ip_location"])
for a in biggest:
body = call("/v1/weibo/users/get", id=a["id"], view="full")
if body:
u = body["data"]["item"]
out.writerow([u["id"], u["name"], u["followers"], u["following"], u["posts"], u["verified"],
u["ext"].get("verified_reason"), u["ext"].get("ip_location")])
board = call("/v1/weibo/trends/list")["data"]["items"]
on_board = [t for t in board if QUERY in t["name"]]
print(f"{len(posts)} posts, {len(biggest)} profiles;", "on the board:" if on_board else "not on the board",
", ".join(f"#{t['rank']} {t['name']}" for t in on_board))
That run is the worked example: 3 search pages, 5 profiles and one board read, 9 calls. A daily version for a month (the same 9 calls each day) is $0.54.
Good to know
- What search shows. Results are what Weibo's own search returns; posts Weibo hides from search do not appear, and search stops paging at the depth Weibo's search does.
- Long posts. Search can cut long posts short. Check
ext.is_long_text(withview=full); the full text is on the post's page aturl. - Nulls. Shares and favorites are not shown on Weibo posts, so those
metricsare null; total likes received is null on profiles.meta.unavailablenames each null field. - Not in the catalog. Comment threads and an account's own timeline are not covered; search, profiles and the board are.
- UIDs. Profiles take the numeric UID only, not a nickname or link.
- Free cases. Deleted or banned accounts answer
not_found, which is free, like rate limits and every other error. The same request with the sameIdempotency-Keywithin 24 hours returns the stored answer withmeta.replayed: true.
To watch the board itself, open the Weibo hot search tool. For tracking what people say about a brand over time, see word of mouth.
What the example costs
3 pages of the newest posts about 预制菜 (prepared meals), the profiles of the 5 authors with the most followers, and one read of the hot search board
| Endpoint | Calls | Price | Subtotal |
|---|---|---|---|
| Weibo post search | 3 | $0.002 | $0.006 |
| Weibo account profile | 5 | $0.002 | $0.01 |
| Weibo hot search | 1 | $0.002 | $0.002 |
| Total | 9 | $0.018 |
Prices in force when this page was built. A failed call costs nothing.
In your AI (MCP)
Connect the MCP server once, then say what you want. The agent finds these endpoints, reads their prices and calls them on your balance.
SaySearch Weibo for the newest posts about 预制菜, three pages. Find the 5 authors with the most followers and give me their profiles, then tell me whether the topic is on the hot search board right now.
What the agent sends to the call tool first
{
"endpoint": "weibo.posts.search",
"params": {
"query": "新品发布"
},
"idempotency_key": "<a new UUID>"
}