Answers

Bilibili's search API keeps returning -412. Is there a simpler way to search videos?

Short answer

Use Bilibili video search on okdata: you send a keyword and an API key, with no wbi signature and no buvid3 cookie to keep fresh. Each call returns a page of about 20 videos for $0.002, so 1,000 results cost $0.10. If Bilibili pushes back, you get a retryable error and pay nothing.

A -412 from Bilibili means its risk control has decided your traffic looks automated. The usual story: a script that searched for a keyword every hour worked fine for a week, then the wbi keys rotated, the buvid3 cookie went stale or too many requests came from one IP, and every call now ends in -412 or -352. You can keep patching the signer, or you can hand the keyword to an API that deals with that side and gives you clean results.

The endpoint

Bilibili video search takes query and an optional sort: relevance (the default), latest or most_viewed. Each result carries the BV id, link, title, description, publish time, the uploader's name and mid, views, likes, comment count, favorites and tags. Add view=full and you also get the aid, duration, danmaku count and section name under ext.

Need coins or shares for a particular hit? Those are not in search results. Bilibili video detail takes the BV id and returns them for $0.002.

What it costs

Job Calls Cost
One page, about 20 videos 1 $0.002
Ten pages for one keyword 10 $0.02
1,000 videos 50 $0.10
5 keywords, first page, daily for 30 days 150 $0.30

Every page is its own call and its own charge. Errors are free, including the ones where Bilibili refused the request upstream.

Python, with polite retries

import os, time, uuid, requests

URL = "https://api.okdata.online/v1/bilibili/videos/search"
H = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}

def search(query, sort="relevance", cursor=None):
    params = {"query": query, "sort": sort, "view": "full"}
    if cursor:
        params["cursor"] = cursor
    key = str(uuid.uuid4())  # same key on every retry, so a retry never pays twice
    for _ in range(4):
        body = requests.get(URL, headers={**H, "Idempotency-Key": key}, params=params, timeout=60).json()
        if body["code"] == "ok":
            return body
        if body.get("retry") == "yes":
            time.sleep(body.get("retry_after_seconds") or 3)
            continue
        raise RuntimeError(f"{body['code']}: {body['message']}")
    raise RuntimeError("still failing after 4 tries")

page = search("Claude Code 教程", sort="most_viewed")
for v in page["data"]["items"]:
    print(v["id"], v["metrics"]["views"], v["ext"]["danmaku"], v["title"])

Limits worth knowing

  • Bilibili stops serving search pages at a certain depth, as the site itself does. For a broad topic, run several narrower keywords instead of paging forever.
  • Shares are not part of search results; that field is null and meta.unavailable names it.
  • Only public videos come back. Nothing here logs in or sees what a signed-in user would.
  • Error codes, and which ones are worth retrying, are listed on the errors page.

When doing it yourself is the better call

The community docs in bilibili-API-collect explain wbi signing step by step, and the open-source Python library bilibili-api handles signing and cookies for you. If you need endpoints this catalog does not cover, or you run very high volume on infrastructure you already maintain, that route costs nothing but your time. For a one-off list of twenty videos, the search box on bilibili.com is fine. If you are collecting videos for a content study, the content research guide shows how search, detail and comments fit together.

Updated