Requests and responses
How every okdata call is shaped: paths, auth, the response envelope, paging with cursors, view and fields to trim output, and idempotent retries.
Paths and methods
Every endpoint lives at https://api.okdata.online/v1/ followed by its id with dots as slashes. douyin.videos.comments.list is GET https://api.okdata.online/v1/douyin/videos/comments/list. Parameters go in the query string. An endpoint that takes a list (ids, keywords) is a POST with a JSON body instead; its page says so.
Auth
Send the key as a bearer token:
Authorization: Bearer okd_...
The key never goes in the URL. The catalog, the status route and the MCP tools find and describe work without a key.
The envelope
Every success has the same outer shape:
{
"code": "ok",
"request_id": "0f6c2a8e-4b1d-4e0a-9a51-3c7d2b9e1f20",
"data": { "items": [] },
"page": { "next_cursor": "eyJvIjoyMH0", "has_more": true },
"warnings": [],
"meta": {
"capability": "douyin.videos.search",
"billing": { "amount": "0.004", "currency": "USD", "balance_after": "9.996" },
"unavailable": { "metrics.views": "platform" },
"cached": false,
"replayed": false,
"fetched_at": "2026-11-11T08:00:00Z"
}
}
- List endpoints return
data.itemsand apage. Single-object endpoints returndata.itemand nopage. meta.billing.amountis the charge for this call, as a decimal string in US dollars. Do money maths on it as a decimal, never as a float.meta.unavailablemaps each null field to a reason:platform(the platform never shows it) orcapability(we cannot fetch it yet).meta.cachedis true when the answer came from a short-lived cache;meta.fetched_atsays when the data was read from the platform.
Paging
One call returns one page. To get the next one, send page.next_cursor back as cursor, unchanged. page.has_more: false means you have everything. Each page is a separate call with its own charge, so stop as soon as you have enough:
import os
import requests
URL = "https://api.okdata.online/v1/douyin/videos/comments/list"
HEADERS = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}
def comments(video_id, max_pages=5):
cursor = None
for _ in range(max_pages):
params = {"id": video_id, **({"cursor": cursor} if cursor else {})}
body = requests.get(URL, headers=HEADERS, params=params, timeout=60).json()
if body["code"] != "ok":
raise RuntimeError(body["message"])
yield from body["data"]["items"]
if not body["page"]["has_more"]:
return
cursor = body["page"]["next_cursor"]
Five pages of Douyin comments are five calls: $0.01.
view and fields
Long responses cost tokens when a model reads them. Two parameters trim them:
view=compactreturns fewer fields per object,standardis the default, andfulladds platform-specific extras underext.fields=id,text,metrics.likeskeeps only the fields you name, with dots for nested fields.
Neither changes the price.
Retrying safely
Send an Idempotency-Key header with every call, a random string you keep with the request. Sending the same key with the same request again within 24 hours returns the stored result with meta.replayed: true and no new charge. The same key with a different request is rejected as invalid_params. On rate_limited, wait retry_after_seconds; on not_found, do not retry at all. The errors page lists every case.
Updated 2026-11-11