Zhihu Expert Discovery API Workflow | SandBase
Build a Zhihu expert-discovery workflow: search users, read a profile, and pull their answers — one SandBase key, no Zhihu login.

Finding domain experts on Zhihu comes down to three moves: search users in a topic, read a candidate’s public profile, and skim the answers they have written. This tutorial wires those three moves into one Zhihu expert discovery API workflow using the SandBase Zhihu API — no Zhihu login, no scraper. The endpoint reference guarantees the response envelope (id, status, model, and outputs[0].data); the business fields shown below are illustrative structure, not a guaranteed schema, so confirm them against a live response.
If you want the full endpoint tour first, start with the Zhihu public data API hub. This piece is the applied workflow.
Key takeaway
- Three steps:
user-search-v3(search) →user-info(profile) →user-answers(answers).- Every call is
POST /v1/api/zhihu/<path>with oneSANDBASE_API_KEY; responses share the{ id, status, model, outputs }envelope.- Search takes a
keyword; the profile and answers reads take auser_url_tokenyou extract from search results.- Public, read-only data only; there is no login or posting on your side.
The workflow at a glance
| Step | Endpoint | Input | You get |
|---|---|---|---|
| 1. Search users | zhihu/web/user-search-v3 | keyword | matching users with a url_token |
| 2. Read the profile | zhihu/web/user-info | user_url_token | name, follower/answer counts |
| 3. Pull their answers | zhihu/web/user-answers | user_url_token (plus paging params such as offset/limit/cursor per the schema) | a page of the user’s answers |
The endpoint API reference is the source of truth for each parameter name and response path.
Step 1 — Search users in a topic
Start with a shared helper that branches on status, then search users and grab a url_token:
import os
import requests
BASE = "https://api.sandbase.ai/v1/api/zhihu"
HEADERS = {
"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
"Content-Type": "application/json",
}
def call(path: str, payload: dict) -> dict:
resp = requests.post(f"{BASE}/{path}", headers=HEADERS, json=payload, timeout=90)
resp.raise_for_status()
body = resp.json()
if body.get("status") != "completed":
raise RuntimeError(body.get("error", {}).get("message", "request did not complete"))
# Only the envelope is guaranteed; read outputs[0].data defensively.
return (body.get("outputs") or [{}])[0].get("data", {})
# The nested business fields (object/url_token/headline/follower_count) are
# illustrative structure, not a guaranteed schema, so read them with .get().
search = call("web/user-search-v3", {"keyword": "人工智能"})
results = search.get("data", [])
first = results[0].get("object", {}) if results else {}
url_token = first.get("url_token")
print(first.get("headline"), first.get("follower_count"), "->", url_token)
Each search result wraps the user under an object with fields like url_token, headline, follower_count, and answer_count. When present, those counts give you a first-pass filter for who is worth reading. These are illustrative structure rather than a guaranteed schema — confirm them against a live response.
Step 2 — Read the profile
Read the full profile with user-info:
profile = call("web/user-info", {"user_url_token": url_token})
# These business fields are illustrative, not a guaranteed schema; use .get().
print(profile.get("name"), profile.get("follower_count"), profile.get("answer_count"))
The profile read may return fields like name, headline, follower_count, answer_count, and articles_count. These are illustrative structure rather than a guaranteed schema, so confirm them against a live response. When present, they let you rank candidates by reach and output before you pull their answers.
user-info returns name, follower count, and answer count for a user.
Step 3 — Pull their answers
Read a page of the user’s answers with user-answers. Paging is driven by request parameters — depending on the endpoint’s schema, that is an offset/limit pair or a cursor you send on the next call, not a field you follow inside the response:
# Paging is a request parameter. Check the endpoint schema for the exact names;
# many Zhihu list endpoints accept offset/limit (some use cursor). To read the
# next page, increment offset on the next call rather than reading a response field.
result = call("web/user-answers", {"user_url_token": url_token, "offset": 0, "limit": 20})
answers = result.get("data", [])
print(len(answers), "answers on this page")
The answers response nests a data list of answer items. The block below is an illustrative response shape — the business fields are example structure, not a guaranteed schema, so treat the field names and values as examples and confirm them against a live response:
{
"id": "876962b1-9b17-409f-a654-4a5004199e31",
"status": "completed",
"model": "zhihu/web/user-answers",
"outputs": [
{
"data": {
"data": [
{ "answer_type": "…", "content": "…", "comment_count": 0 }
]
}
}
]
}
user-answers returns the user’s answer feed and a paging block.
Putting it together
A minimal expert-discovery pass looks like this:
search = call("web/user-search-v3", {"keyword": "人工智能"})
report = []
for hit in search["data"][:10]:
user = hit.get("object")
if not user or "url_token" not in user:
continue # search results can mix in non-user objects
report.append({
"name": user.get("name"),
"url_token": user["url_token"],
"followers": user.get("follower_count"),
"answers": user.get("answer_count"),
})
# rank by followers/answers, then call user-answers for the ones worth reading
Because every call shares the same envelope and the same call helper, adding retries or rate-limit backoff is a one-place change. To read past the first page of answers, follow the paging block per the schema. When you need more than these reads, check the live Zhihu listing for the endpoint that fits and confirm its parameters before wiring it in.
Handling the rough edges
- Extract the
url_tokenfrom the search object.user-search-v3wraps each user under anobject, and results can mix in non-user types — guard forurl_tokenbefore you use it. - Rank before you read. Use
follower_countandanswer_countfrom search oruser-infoto pick candidates before spending calls onuser-answers. - Page with request parameters.
user-answerspages through request parameters — anoffset/limitpair or acursorper the schema — so increment the parameter on the next call rather than assuming one page is the whole feed. - Branch on
status. Afailedortimeoutrun carrieserrorand nooutputs. Thecallhelper already enforces this. - Respect rate limits. As a client-side resilience measure, retry with backoff on transient errors such as HTTP 429.
- Public data only. No login, posting, or private/account-only content.
Why run this at the API layer
You could search Zhihu in a browser and eyeball profiles by hand, but that does not scale and it does not give you structured data to rank. Running these three calls on a schedule turns qualitative browsing into a measurable signal: candidates you can dedupe by url_token, follower and answer counts you can sort on, and answer feeds you can pull in full for topical analysis. Because the calls return named JSON fields, each pass drops cleanly into a table you can diff against the last one — new experts surfacing on a topic, growth in reach, and shifts in who is answering. Scoring an expert’s relevance from their answer text is a separate analysis step you run on top of the collected data.
Composing the workflow
The same uniform envelope keeps this composable. Swap the topic keyword for any niche, add a fourth read — user-articles for a user_url_token, say — and it slots in behind the same call helper with the same status check. You can also widen the funnel: run user-search-v3 across several related keywords in one pass, dedupe the candidates by url_token, and rank the combined set by follower and answer counts before you pull any answers, so calls go only to the experts worth a closer look. Because the reads share one shape, moving from a quick script to a scheduled job is mostly a matter of adding backoff and a place to store each pass.
FAQ
Do I need a Zhihu login or OAuth?
No. You authenticate to SandBase with your SANDBASE_API_KEY, and searching users, reading a profile, and pulling answers need no Zhihu account or OAuth on your side. You still supply a SandBase API key to make the calls.
How do I page through search results and a user’s answers?
Pass the pagination request parameter each endpoint accepts — an offset/limit pair on the next call — rather than assuming a fixed page size. Check the endpoint reference for the exact parameter names and confirm them against a real response before you loop.
Can I read private or account-only data this way?
No. These are public, read-only reads — no private or account-gated data. And the endpoint reference guarantees the { id, status, model, outputs } envelope; the business fields inside vary, so map them from a real response.
Next steps
You now have a repeatable expert-discovery workflow built on three public, read-only calls.
- Read the Zhihu public data API hub
- Get a SandBase API key
- API references: user-search-v3, user-info, and user-answers