# Torii API — agent / LLM skill guide A single-file, task-oriented reference for using the Torii crawler API from code or an agent. Torii crawls 19 social platforms and returns ONE normalized shape. You submit a task, poll it, and read a normalized Envelope. Nothing here ranks, scores, or fabricates: an empty answer and a bot-check are different values you must branch on. Raw copy of this file: GET https://apidoc.torii.t.bldr.tw/torii-api-agent.txt (plain text; safe to fetch and read directly) ==================================================================== 1. BASE URL + AUTH ==================================================================== Base: https://api.torii.t.bldr.tw All routes under /v1. Auth: header `Authorization: Bearer ` (capital "Bearer"; a lowercase prefix is rejected). Scopes: read (crawl) · write (publish) · admin. A read token cannot publish. Public (no token): GET /v1/media, GET /v1/preview, GET /v1/health. ==================================================================== 2. THE CORE LOOP — submit → poll → read ==================================================================== Every crawl is an async task. (a) SUBMIT POST /v1/tasks {source, action, params} -> 202 {task_id, status:"queued"} (b) POLL GET /v1/tasks/{id} -> {status, result?} (c) READ when status == "succeeded", read `result` (the Envelope). Statuses: queued | running | succeeded | failed | cancelled. `succeeded` carries `result`. `failed` is a job error (rare) — DISTINCT from a `blocked` envelope, which is a successful task whose `result.source == "blocked"`. INLINE FAST PATH: add `?wait=8000` to POST /v1/tasks to block up to 8s for the result inline (still returns a task_id to poll if it isn't done in time). CANCEL: POST /v1/tasks/{id}/cancel (or DELETE /v1/tasks/{id}) — frees the account's slot. RETRY: POST /v1/tasks/{id}/retry STREAM: GET /v1/tasks/{id}/stream (SSE) ==================================================================== 3. SUBMIT — source, action, params ==================================================================== source : the platform, e.g. youtube tiktok douyin weibo bilibili instagram threads x facebook truthsocial rumble snapchat dailymotion niconico chzzk rutube twitch odysee kick. action : search | posts | user | detail | comments | hot | recommended | trending | download | post.create | comment.create | delete. NOT every platform supports every action — ask the catalog (section 8), don't assume. An unsupported action returns 404 unsupported. params : object. Common fields: query keyword (search) OR a creator/post ref (posts/user/detail/comments). A ref may be a URL, an @handle, a bare id, or a share link — Torii resolves it. limit max items (default 24). cursor pagination token from a previous response's next_cursor (bypasses cache). max_items posts only: walk a creator's back-catalog by ITEM count. -1 = the whole account. profile which stored login to crawl as (platforms that need auth). Default "default". media video | image | any — filter by media type. preview "eager" — pre-render the hover/scrub preview for each new video item at crawl time. min_duration_s / max_duration_s / orientation / min_width / min_height — server-side filters. force bypass cache, crawl live. max_age_s serve from cache only if the newest matching result is fresher than N seconds. category feed tab (hot/recommended on some platforms; GET /v1/categories lists them). idempotency_key / confirm / dry_run — write verbs (section 7). Example: curl -X POST https://api.torii.t.bldr.tw/v1/tasks -H "Authorization: Bearer $T" -H "content-type: application/json" \ -d '{"source":"douyin","action":"posts","params":{"query":"@qinyao","max_items":300,"media":"video"}}' ==================================================================== 4. RETURN SHAPES — there are FIVE, keyed on the action (not the platform) ==================================================================== The `result` shape depends on what the action returns: A. Item cards search · posts · hot · recommended · detail -> Envelope of Item[] (section 5). detail returns ONE item WITH a playable media[].url; the flat search/posts harvest is metadata-only on some platforms (no stream until detail). B. Comments comments -> {source, total, has_more, cursor?, ms, items:[Comment]} (NO top-level `ok`; branch on `source`). C. Creator user -> {ok, source, items:[], ms, data:{platform, author:{…}}} (a profile, NOT cards). D. Trend words trending -> {ok, source, items:[], ms, data:{platform, trends:[{word,rank,hot_value?,url?}]}} (words, NOT posts). E. Write result post.create · comment.create · delete -> {ok, state, url?, source, ms} (section 7). ==================================================================== 5. THE ENVELOPE + ITEM (return shape A) ==================================================================== Envelope: ok bool — true on a successful crawl (INCLUDING an honest empty); false when error set. source string — provenance/outcome: live | cache | empty | blocked. *** empty != blocked *** empty = a real no-results answer (count:0, ok:true). Render as "nothing". blocked= a captcha / risk control (ok:false, retryable). Do NOT prompt re-login. count int — number of items. items Item[] — the normalized cards. next_cursor string — pass back as params.cursor for the next page. age_s int — for source:"cache", how old the cached result is. ms int — crawl duration. error object? — {code, message, retryable}. Only when ok:false. Branch on code, never message. warnings [] — non-fatal notes (e.g. a filter dropped items lacking the metadata). Item (the stable consumer projection — never ranked/scored; stats are the platform's raw numbers): id "provider:nativeid", globally unique + stable (e.g. "douyin:7412…"). type video | image | text. provider the platform. title, text title (where the platform has one) and caption/body. author, author_id, author_handle, author_url creator identity; author_url derived centrally. url the post's canonical URL. thumb a torii-hosted cover URL (/v1/media?u=…). NEVER a raw CDN URL. media [{kind:"video"|"image", url, w, h, duration_s}]. URLs are torii-hosted. A playable stream appears on detail; the flat harvest is metadata-only for some platforms. preview on video items: {url, w, h, seconds, status}. A low-res, muted, full-length (<=240s) scrubbable clip. status: hit | rendered | pending. See section 6. ts created-at, unix seconds. duration_s video length (omitted when unknown). aspect {width, height} of the media. stats {likes, comments, shares, views} — verbatim platform numbers. provenance per-item origin (live | cache). Comment (return shape B): {cid, text, likes, reply_total, user_id, sec_uid?, handle, user_name, created_at, source, url?, parent_id?}. parent_id set => it is a threaded reply to that cid. Author (return shape C, data.author): {id, source, handle, name, bio, avatar, url, followers, following, posts, likes, verified}. A field the platform doesn't expose is 0/absent — a 0 means "not reported", not a hard zero. Trend (return shape D, data.trends[]): {word, source, rank, hot_value?, url?}. ==================================================================== 6. MEDIA + PREVIEW (all torii-hosted; never a raw CDN URL) ==================================================================== Origin CDN URLs are session/IP-bound and expire in ~1h, so Torii proxies everything: GET /v1/media?u= image proxy (public). GET /v1/media?u=&kind=video&platform=&profile= video proxy (session-gated → home node). GET /v1/preview?platform=

&id= the hover/scrub preview clip (PUBLIC). Cold path: 202 {status:"rendering"} + Retry-After — poll the SAME url until 200. A 200 is a complete, playable mp4 (Range/206 supported). 404 {code:"preview_unavailable", reason} = terminal (expired|blocked|session_expired|no_stream|unsupported); a transient slow render answers 202 again. POST /v1/preview/warm {items:[{platform,id}]} pre-warm a page of previews (gated). `preview` attaches to an Item IFF it carries a playable video. `preview:"eager"` in params pre-renders at crawl time (safe fleet-wide; a no-op on items without a stream). YouTube's flat search/posts harvest is metadata-only, so preview attaches there only via detail. ==================================================================== 7. WRITE VERBS (post.create / comment.create / delete) ==================================================================== Irreversible + outward-facing, so they are gated: - token must carry the `write` scope. - first call returns 409 {status:"needs_confirmation"}; re-send with "confirm": true. - ONE attempt, never auto-retried. - send "idempotency_key": "" so a timed-out retry replays instead of double-posting. - "dry_run": true validates WITHOUT posting (returns state:"planned"). Result (WriteResult): {ok, state, url?, target?, source, ms, error?}. state: planned | published | deleted | session_expired | blocked | rejected | duplicate. duplicate = an in-flight retry of the same idempotency_key — poll the original. Which platforms: threads, truthsocial, weibo (post.create / comment.create / delete). See the catalog. ==================================================================== 8. DISCOVER CAPABILITIES — don't hardcode, ask the catalog ==================================================================== GET /v1/task-sources every {name:"source.action", group, summary, params[], confirm?, write?}. If a "source.action" is not listed, it genuinely doesn't exist (=> unsupported). `params` lists what each action accepts (e.g. which verbs honour `preview`). GET /v1/capabilities the same, grouped by provider with per-provider actions[]. GET /v1/categories?source=

the feed tabs a platform offers (for hot/recommended `category`). GET /v1/profiles stored accounts and their health (the re-login signal). A profile marked session_expired needs a human re-login; a per-task `blocked` does NOT. Drive your taxonomy off /v1/task-sources: the advertising is correct, so a capability-driven caller is automatically right, and you never call an action that returns `unsupported`. ==================================================================== 9. ERROR CODES (branch on error.code, never the message) ==================================================================== invalid_request 400 fix the params auth_required 401 attach a valid bearer forbidden_scope 403 use a token with the scope unsupported 404 the platform lacks this action — stop asking not_found 404 the post is gone (deleted/never existed) — terminal rate_limited — platform throttled (429/418) — back off, retry later blocked — bot-check/captcha/risk control — retryable; do NOT prompt re-login session_expired — the stored login lapsed — re-login the profile, then retry timeout — slow upstream — back off, retry duplicate — a write idempotency_key is in flight — poll the original / retry the same key upstream_error — unclassified upstream failure — retry `blocked` and `session_expired` are deliberately distinct: blocked is transient (retry the task); only session_expired (surfaced on /v1/profiles) means the ACCOUNT needs a human re-login. ==================================================================== 10. RULES OF THUMB FOR AN AGENT ==================================================================== - Branch on `source` (live/cache/empty/blocked) and `error.code`, never on the message or an empty list. - empty (count:0, ok:true) is a real answer — do NOT invent an explanation for it. - To walk a creator's whole back-catalog: action:"posts" with max_items (or -1). Torii owns the walk and paces it; you just read `items`. To page manually, round-trip `next_cursor` as `params.cursor`. - Prefer action:"posts" over "search" when you want a known creator — it is not keyword-quota'd. - Media URLs are torii-hosted and never expire in your hands — store the torii URL, not the origin. - A video Item's `preview` is a hover/scrub clip; the full stream is on the Item's `media[]` (detail). - Use ?wait= for a fast single crawl; poll for a deep walk (max_items can take 1–2 min, paced).