# 翻页：cursor、分页和每一页的费用

> 在 okdata 里翻页：一次调用是一页、扣一次费，page.next_cursor 和 has_more 的含义，cursor 的使用规则，带页数上限的 Python 循环，cursor 过期怎么办，以及 POST 批量查询。

网页版：https://okdata.online/zh/docs/pagination/

列表接口一次调用返回一页，每一页都单独按一次调用计费。要下一页，就把 `page.next_cursor` 作为 `cursor` 传回同一个接口，其他参数保持不变。`page.has_more` 为 false 就说明取完了；数据够用了也可以随时停下，你没要的页不会收费。

## 一次调用 = 一页 = 一次扣费

列表接口的返回里，`data.items` 旁边总有一个 `page`：

```json
"page": { "next_cursor": "c1.k3Vq9TzR0bYw2LmXe8NdPa", "cursor_expires_at": "2026-11-13T08:00:00Z", "has_more": true, "returned": 20 }
```

- `next_cursor` 是一个不透明的字符串。原样传回即可，不要解析它、自己拼它，也不要当成数字存。
- `cursor_expires_at` 是这个 cursor 失效的时间；`returned` 是这一页的条数。
- `has_more` 为 `true` 当且仅当 `next_cursor` 有值。`false` 加上 `next_cursor: null`（`cursor_expires_at` 也是 `null`），表示这个列表没有下一页了。

[抖音视频评论](/zh/apis/douyin/videos/comments/list/)一页大约 20 条，$0.002，所以翻十页就是十次调用，共 $0.02。按条计费的列表接口，费用跟着每页实际条数走：[X 账号粉丝](/zh/apis/x/users/followers/list/)每页满 200 个账号，五个满页共 $0.10。按次和按条两种计费方式的区别，见[计费说明](/zh/docs/billing/)。

这里没有页码。平台自己的信息流只知道“某个位置之后是什么”，所以想看第 5 页，就得先把第 1 到第 4 页取一遍。也有少数列表接口根本没有 `cursor` 参数，比如相关关键词，一次就返回全部结果；接口页的参数表里看得出来。

## cursor 的规则

一个 cursor 只能接着同一个列表往下翻。这些检查都在扣钱之前完成：

- **同一个账户。** 拿到 cursor 的账户下的任何一把 Key 都能用它。
- **同一个接口、同样的参数。** 换了搜索词、id 或筛选条件，cursor 就对不上了。`view` 和 `fields` 每页都可以改，因为它们只影响返回的样子，不影响取什么数据。
- **在有效期内。** 每个可翻页接口在目录里都写着 cursor 的有效期 `cursorTtlSeconds`（在 `/openapi.json` 里是 `x-cursor-ttl-seconds`）。目前所有可翻页接口都是 86,400 秒，也就是 24 小时。

用不了的 cursor 会被拒绝，每种情况有自己的错误码，都是 `charged: false`，并带 `param: "cursor"`：

| code | HTTP | 含义 | 怎么办 |
| --- | --- | --- | --- |
| `cursor_invalid` | 400 | 不是我们发出的 cursor，或者是发给别的账户的 | 把 `page.next_cursor` 原样传回 |
| `cursor_mismatch` | 400 | 发到了别的接口，或者参数变了 | 按它所在那一页的参数重新发 |
| `cursor_expired` | 410 | 超过了有效期（`page.cursor_expires_at`） | 不带 `cursor`，从头开始取 |
| `cursor_unavailable` | 502 | 只有返回上一页的数据源能接着翻，它现在不可用 | 稍后用同一个 cursor 重试，或者不带 cursor 从头开始 |

从头开始会把前面几页重新取一遍，也会重新计费；这期间平台顶部可能又多了新内容，所以请记下已经拿到的 id，跳过重复的条目。把 cursor 存起来、第二天再接着跑的任务，就会遇到 `cursor_expired`；一口气翻完的任务则不会。

## 带页数上限的循环

下面的代码逐页读取抖音评论，最多取 `max_pages` 个计费页；如果 cursor 过期，就从头再来：

```python
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=10, cursor=None):
    seen, pages = set(), 0
    while pages < 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"] == "cursor_expired":
            cursor = None  # 被拒绝不扣费：从头开始
            continue
        if body["code"] != "ok":
            raise RuntimeError(f"{body['code']}: {body.get('problems') or body['message']}")
        pages += 1
        for item in body["data"]["items"]:
            if item["id"] not in seen:
                seen.add(item["id"])
                yield item
        if not body["page"]["has_more"]:
            return
        cursor = body["page"]["next_cursor"]

for c in comments("7301234567890123456", max_pages=3):
    print(c["id"], c["text"][:60])
```

三页最多花 $0.006。每一页都是一个新请求，所以如果你带了 `Idempotency-Key`，每页要用一个新的 key，只有重发同一页时才沿用原来那个。

## has_more 提前变成 false

按条计费的列表接口，第一页如果余额付不起满页，会缩到余额够付的条数，`warnings` 里会写明，而且即使后面还有数据，这一页也不会给下一页的 cursor。充值后从头再取，就能拿到满页。之后的每一页都要有付得起满页的余额，不够时返回 `402 insufficient_balance`，不扣费。

## 用 POST 批量查询

少数单对象接口也能一次传一组 id：目录里它们的 `post` 为 `true`，一次最多能传几个，看 `/prices.json` 里的 `batch_max`（`ids` 参数的说明里也写着）。[X 账号信息](/zh/apis/x/users/get/)一次最多 100 个：

```bash
curl -s "https://api.okdata.online/v1/x/users/get" \
  -H "Authorization: Bearer $OKDATA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["4398626122", "11348282"]}'
```

批量调用的返回里，`data.items` 按查到的顺序排列，`data.missing` 列出没查到的 id，每个都带 `reason`。批量调用没有 `page`，也没有 cursor。按条计费的批量调用按查到的对象收费，最多 $0.04；按次计费的批量调用，比如[抖音星图达人资料](/zh/apis/douyin/xingtu/creators/get/)，一次最多 50 个 id，只收一次 $0.004。一个都没查到的批量调用不收费；传的 id 超过接口上限，会在预扣之前就返回 `invalid_params`。请求的其他规则见[请求说明](/zh/docs/requests/)。

更新于 2026-11-12
