> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sorsa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ページネーション

Sorsa APIで一覧を取得すると、結果はページに分かれて返されます。すべてのデータを取得するには、カーソルを使って、続きがなくなるまでページを順に取得します。

***

## カーソルによるページネーションの仕組み

Sorsaは従来のページ番号の代わりにカーソル方式を使います。新しい内容が常に追加されるソーシャルメディアのデータでは、オフセット方式だと取得漏れや重複が起きるため、カーソル方式のほうが信頼性が高くなります。

ページネーション対応のすべてのエンドポイントで、手順は同じです。

1. 最初のリクエストをカーソルなしで送信します。
2. データと`next_cursor`フィールドが返されます。
3. 次のリクエストに`next_cursor`の値を渡し、次のページを取得します。
4. `next_cursor`が`null`またはレスポンスに存在しなければ、終端です。

すべてのエンドポイントがページネーションを使うわけではありません。`/info`、`/tweet-info`、`/score`、`/about`などは1つのオブジェクトを返し、カーソルはありません。`/info-batch`、`/tweet-info-bulk`や、`/top-followers`などの暗号資産分析の一覧も、1回のレスポンスで結果を返します。各エンドポイントのリファレンスで`next_cursor`パラメーターの有無を確認してください。

***

## レスポンスの構造

ページ分割されたレスポンスは、次のいずれかの形式です。

```text theme={null}
{
  "users": [ ... ],
  "next_cursor": "DAABCgABF7Y..."
}
```

```text theme={null}
{
  "tweets": [ ... ],
  "next_cursor": "DAABCgABF7Y..."
}
```

これらのユーザー・ツイート一覧では、データは`users`または`tweets`に入ります。`next_cursor`は中身を解釈せず、そのまま渡してください。null、空、または存在しない場合は終了します。カーソルを増分したり、JavaScriptのNumberに変換したりしないでください。

ラッパーとオブジェクトのスキーマについては、[レスポンス形式](https://docs.sorsa.io/ja/response-format)を参照してください。

***

## カーソルの渡し方

フィールド名は常に`next_cursor`です。エンドポイントによって違うのは指定場所だけです。GETではクエリパラメーター、POSTではJSON本文に渡します。

**GETエンドポイント**（`/followers`、`/follows`、`/list-tweets`など）では、`next_cursor`をクエリパラメーターに指定します。

```bash theme={null}
# First page
curl --request GET \
  --url 'https://api.sorsa.io/v3/followers?username=elonmusk' \
  --header 'ApiKey: YOUR_API_KEY'

# Next page
curl --request GET \
  --url 'https://api.sorsa.io/v3/followers?username=elonmusk&next_cursor=DAABCgABF7Y...' \
  --header 'ApiKey: YOUR_API_KEY'
```

**POSTエンドポイント**（`/search-tweets`、`/user-tweets`、`/comments`など）では、`next_cursor`をJSON本文に指定します。

```bash theme={null}
# First page
curl --request POST \
  --url 'https://api.sorsa.io/v3/search-tweets' \
  --header 'ApiKey: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"query": "bitcoin"}'

# Next page
curl --request POST \
  --url 'https://api.sorsa.io/v3/search-tweets' \
  --header 'ApiKey: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"query": "bitcoin", "next_cursor": "DAABCgABF7Y..."}'
```

***

## ページの件数は固定ではありません

Xのデータの性質上、1ページに返される件数は変動します。最大20件を返すエンドポイントでも、18件、12件、場合によっては5件しか返さないページがあり、その先にまだデータが続くことがあります。

**件数だけで終端を判断しないでください。** 想定より少ない件数でも、続きがないとは限りません。必ず`next_cursor`を確認してください。存在し、`null`でなければ、次のページを取得できます。

***

## ページネーションの実装例

これらの例では、結果をメモリに蓄積します。大規模な処理では、各ページをストレージに書き込み、文字列のIDで重複を除去し、保存後にチェックポイントを記録してください。カーソルの利用中は、対象アカウント、検索条件、フィルター、並び順を変えないでください。ページ数やリクエスト数の上限を設定し、同じカーソルの繰り返しを検出して、意図しない無限処理を防ぎます。1つのループに待機時間を入れるだけでは、同じAPIキーを使う他のワーカーとの速度調整はできません。

**Python：フォロワーを順に取得する（GET）**

```python theme={null}
import time
import requests

API_KEY = "YOUR_API_KEY"

def fetch_all_followers(username):
    all_users = []
    cursor = None

    while True:
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor

        response = requests.get(
            "https://api.sorsa.io/v3/followers",
            params=params,
            headers={"ApiKey": API_KEY},
            timeout=30,
        )
        response.raise_for_status()
        data = response.json()

        users = data.get("users", [])
        all_users.extend(users)
        print(f"Page fetched: {len(users)} users. Total so far: {len(all_users)}")

        cursor = data.get("next_cursor")
        if not cursor:
            break

        time.sleep(0.05)  # respect 20 req/s rate limit

    return all_users

followers = fetch_all_followers("elonmusk")
print(f"Done. {len(followers)} followers total.")
```

**Python：検索結果を順に取得する（POST）**

```python theme={null}
import time
import requests

API_KEY = "YOUR_API_KEY"

def search_all_tweets(query):
    all_tweets = []
    cursor = None

    while True:
        body = {"query": query}
        if cursor:
            body["next_cursor"] = cursor

        response = requests.post(
            "https://api.sorsa.io/v3/search-tweets",
            json=body,
            headers={"ApiKey": API_KEY},
            timeout=30,
        )
        response.raise_for_status()
        data = response.json()

        tweets = data.get("tweets", [])
        all_tweets.extend(tweets)
        print(f"Page fetched: {len(tweets)} tweets. Total so far: {len(all_tweets)}")

        cursor = data.get("next_cursor")
        if not cursor:
            break

        time.sleep(0.05)

    return all_tweets

results = search_all_tweets("bitcoin")
print(f"Done. {len(results)} tweets total.")
```

**JavaScript：フォロワーを順に取得する（GET）**

```javascript theme={null}
async function fetchAllFollowers(username) {
  const API_KEY = "YOUR_API_KEY";
  const allUsers = [];
  let cursor = null;

  while (true) {
    const params = new URLSearchParams({ username });
    if (cursor) params.append("next_cursor", cursor);

    const response = await fetch(
      `https://api.sorsa.io/v3/followers?${params}`,
      { headers: { "ApiKey": API_KEY } }
    );
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();

    const users = data.users || [];
    allUsers.push(...users);
    console.log(`Page fetched: ${users.length} users. Total: ${allUsers.length}`);

    cursor = data.next_cursor;
    if (!cursor) break;

    await new Promise(r => setTimeout(r, 50));
  }

  return allUsers;
}
```

**JavaScript：検索結果を順に取得する（POST）**

```javascript theme={null}
async function searchAllTweets(query) {
  const API_KEY = "YOUR_API_KEY";
  const allTweets = [];
  let cursor = null;

  while (true) {
    const body = { query };
    if (cursor) body.next_cursor = cursor;

    const response = await fetch("https://api.sorsa.io/v3/search-tweets", {
      method: "POST",
      headers: {
        "ApiKey": API_KEY,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();

    const tweets = data.tweets || [];
    allTweets.push(...tweets);
    console.log(`Page fetched: ${tweets.length} tweets. Total: ${allTweets.length}`);

    cursor = data.next_cursor;
    if (!cursor) break;

    await new Promise(r => setTimeout(r, 50));
  }

  return allTweets;
}
```

***

## エラー処理付きのページネーション

本番環境では、ページネーションに再試行処理を組み合わせてください。1ページの取得失敗で、収集処理全体が停止することを防げます。詳しいエラー処理は[エラーコード](https://docs.sorsa.io/ja/error-codes)を参照してください。

```python theme={null}
import time
import requests

API_KEY = "YOUR_API_KEY"

def paginate_with_retries(username, max_retries=3):
    all_users = []
    cursor = None

    while True:
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor

        for attempt in range(max_retries):
            response = requests.get(
                "https://api.sorsa.io/v3/followers",
                params=params,
                headers={"ApiKey": API_KEY},
                timeout=30,
            )

            if response.status_code == 200:
                break
            elif response.status_code == 429:
                time.sleep(1)
                continue
            elif response.status_code >= 500:
                time.sleep(2)
                continue
            else:
                raise Exception(f"Error {response.status_code}: {response.text}")
        else:
            raise Exception("Max retries exceeded")

        data = response.json()
        all_users.extend(data.get("users", []))

        cursor = data.get("next_cursor")
        if not cursor:
            break

        time.sleep(0.05)

    return all_users
```

***

## 次のステップ

* [レート制限](https://docs.sorsa.io/ja/rate-limits)：大量データの取得時に毎秒20リクエストの制限を守る
* [エラーコード](https://docs.sorsa.io/ja/error-codes)：ページネーションのループで429などのエラーに対応する
* [レスポンス形式](https://docs.sorsa.io/ja/response-format)：User・Tweetオブジェクトの完全なスキーマ
