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.items and a page. Single-object endpoints return data.item and no page.
  • meta.billing.amount is 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.unavailable maps each null field to a reason: platform (the platform never shows it) or capability (we cannot fetch it yet).
  • meta.cached is true when the answer came from a short-lived cache; meta.fetched_at says 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=compact returns fewer fields per object, standard is the default, and full adds platform-specific extras under ext.
  • fields=id,text,metrics.likes keeps 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