Douyin Billboard Monitoring API Tutorial
Monitor Douyin's high-like billboard and resolve each video to a stable aweme_id with one SandBase API key — poll the board, dedup by ID, no login, no SDK.

Douyin’s billboards surface what’s spiking right now — but a monitoring job needs more than a snapshot. You want to poll the board on a schedule, recognize which videos you’ve already seen, and key everything to a stable identifier so your dedup and storage stay clean across runs. This tutorial wires exactly that: poll the high-like billboard, resolve each entry to a canonical aweme_id, and hand your agent a deduplicated feed.
One SandBase API key, no Douyin login, no SDK. Each endpoint’s API reference is the source of truth for its parameters and for the response envelope (id/status/model/outputs[0].data); the business-payload field names shown below are an illustrative shape, not a guaranteed schema, so confirm them against a live response.
For the full Douyin endpoint surface, see the Douyin public data API hub. Ready to build? Get a SandBase API key.
Key takeaway
- Two endpoints —
douyin/billboard/hot-total-high-like-listanddouyin/web/aweme-id— read through one API key.- The billboard nests its ranked videos under
data.data.objswith an upstreamcode; each item carriesitem_id,item_title,nick_name, andlike_cnt.- Resolve a video URL to a canonical
aweme_idso dedup and storage key off one stable identifier.- Public, read-only data only. No posting, no login on your side; authenticate with a SandBase API key.
The plan
- Poll the billboard. Call
douyin/billboard/hot-total-high-like-list, check the upstreamcode, and read ranked videos fromdata.data.objs. - Resolve stable IDs. For each video, call
douyin/web/aweme-idto get a canonicalaweme_id. - Dedup and diff. Keep a set of seen IDs; emit only the entries that are new since the last run.
The Douyin API page on SandBase — the billboard cluster includes the high-like list endpoint.
Step 1 — poll the billboard
Every SandBase Model API call is a POST to /v1/api/<vendor>/<path> with your key in the Authorization header. Branch on status before reading outputs, and — because the billboard wraps its result — check the upstream code before reading data.objs.
import os
import requests
SANDBASE = "https://api.sandbase.ai/v1/api"
HEADERS = {
"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
"Content-Type": "application/json",
}
def call(path: str, body: dict) -> dict:
resp = requests.post(f"{SANDBASE}/{path}", headers=HEADERS, json=body, timeout=90)
resp.raise_for_status()
out = resp.json()
if out.get("status") != "completed":
raise RuntimeError(out.get("error", {}).get("message", f"{path} did not complete"))
return out["outputs"][0]["data"]
def billboard() -> list[dict]:
data = call("douyin/billboard/hot-total-high-like-list", {})
if data.get("code") != 0:
raise RuntimeError(data.get("message", "douyin upstream error"))
# The reference only guarantees the envelope; business fields vary by endpoint — confirm against a live response.
return data.get("data", {}).get("objs", [])
The block below is an illustrative response shape — the field names are not a guaranteed schema, so confirm them against a live response, since payloads vary and change over time:
{
"id": "d4727a3b-18ea-403d-b90b-db093210ecac",
"status": "completed",
"model": "douyin/billboard/hot-total-high-like-list",
"outputs": [
{
"data": {
"code": 0,
"data": {
"objs": [
{ "item_id": "…", "item_title": "…", "nick_name": "…", "like_cnt": 0, "item_url": "…" }
]
}
}
}
]
}
The endpoint API reference is the source of truth for each parameter name and response path.
Step 2 — resolve a stable aweme_id
Board entries carry an item_id, but for monitoring you want the canonical aweme_id that a share URL resolves to — it’s the identifier you’ll dedup and store against. Feed the video URL to douyin/web/aweme-id, which returns the ID as a plain string:
def resolve_aweme_id(video_url: str) -> str:
return call("douyin/web/aweme-id", {"url": video_url})
Because call already branched on status, this endpoint returns the ID string directly under data. Pass a Douyin video or share URL and you get back a canonical ID you can trust across runs.
The aweme-id endpoint resolves a video URL to a canonical aweme_id string.
Step 3 — dedup and diff across runs
Now the monitoring loop. Keep a set of seen IDs between runs (in memory here; back it with a store in production). Each poll, resolve IDs, drop anything you’ve seen, and emit only the new entries:
SEEN: set[str] = set()
def poll_new() -> list[dict]:
fresh = []
# The reference only guarantees the envelope; business fields vary by endpoint — confirm against a live response.
for i, obj in enumerate(billboard()):
item_url = obj.get("item_url")
if not item_url:
continue
aweme_id = resolve_aweme_id(item_url)
if aweme_id in SEEN:
continue
SEEN.add(aweme_id)
fresh.append(
{
"aweme_id": aweme_id,
"rank": i + 1,
"title": obj.get("item_title"),
"author": obj.get("nick_name"),
"likes": obj.get("like_cnt"),
}
)
return fresh
for entry in poll_new():
print(f"NEW #{entry['rank']} {entry['title']} — {entry['author']} ({entry['likes']} likes)")
Run it on a schedule and each execution prints only what changed. Because everything keys off the canonical aweme_id, a video that stays on the board across polls is recognized as the same item — no duplicate alerts, no double storage.
Why the uniform envelope matters here
Both endpoints arrive in the same { id, status, model, outputs } envelope, so the fragile part — auth, transport, status handling, retries — lives once in call and is reused for both. The billboard nests its rows under data.data.objs behind an upstream code; the aweme-id endpoint returns a bare string under data. Those are the only two per-endpoint facts your code needs to know, and both are visible in one real response.
That separation keeps the monitor easy to extend. Want to enrich each new entry with more detail? Add one more call behind the same helper, key it by the aweme_id you already resolved, and the dedup logic doesn’t change. Your attention stays on the monitoring signal — what’s newly trending — instead of on parsing quirks. When you need more than the board and the ID, check the Douyin hub for the endpoint that fits and confirm its parameters before wiring it in.
Limitations and boundaries
- Public, read-only data only. No posting, following, or private/account-only data.
- Payload shape and upstream code. The billboard nests under
data.data.objsbehindcode; aweme-id returns a string underdata. Inspect a real response and read the schema first. - Rate and volume. Treat responses as best-effort reads; retry with backoff on transient errors such as HTTP 429, and pace your polling.
- Verify endpoints against the live reference. Availability and fields can change; confirm before building on a specific endpoint.
- Not an official partnership. SandBase provides uniform access to public data; respect Douyin’s terms and applicable rules.
FAQ
Do I need a Douyin login?
No. You authenticate to SandBase with your SANDBASE_API_KEY. These read endpoints need no Douyin account or OAuth on your side.
Why resolve an aweme_id when the board already has item_id?
The canonical aweme_id is the identifier a share URL resolves to — using it for dedup and storage keeps your keys consistent regardless of which URL form you started from.
How often should I poll? Pace it to your needs and handle transient errors with backoff. Treat the board as a best-effort read, not a real-time stream.
Build it
Create a SandBase API key, poll the billboard, resolve IDs, and diff across runs. When you are ready: