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

# Pagination

> Page through list results with opaque cursors

List endpoints return a fixed-size page of results. To fetch the next page, pass the `nextCursor` from the previous response back as `startingAfter`. Cursors are opaque — never parse, persist, or generate them yourself.

## Parameters

<ParamField query="limit" type="integer" default="20">
  Number of results per page. Minimum `1`, maximum `100`.
</ParamField>

<ParamField query="startingAfter" type="string">
  Opaque cursor from the previous response's `nextCursor`. Returns the page of results immediately after that cursor.
</ParamField>

## Response shape

Every list response uses the same envelope:

```json theme={null}
{
  "object": "list",
  "data": [ /* page of objects */ ],
  "hasMore": true,
  "nextCursor": "eyJzIjoiMjAyNi0wNC0yOVQxNDozMjowMC4wMDBaIiwiaSI6InRpXzhrTndxMlR0N3lHa1g5d000YkRmUCJ9",
  "url": "/v1/tickets"
}
```

| Field        | Type           | Description                                                                                 |
| ------------ | -------------- | ------------------------------------------------------------------------------------------- |
| `object`     | string         | Always `"list"`.                                                                            |
| `data`       | array          | The page of results. Empty list is `[]`, never `null`.                                      |
| `hasMore`    | boolean        | `true` if more results exist after this page. Trust this — do not infer from `data.length`. |
| `nextCursor` | string \| null | Opaque cursor for the next page. `null` when `hasMore` is `false`.                          |
| `url`        | string         | The path of the listed resource.                                                            |

## Walking through every page

Loop until `hasMore` is `false`:

<CodeGroup>
  ```bash curl theme={null}
  # Page 1
  curl "https://api.replyful.com/v1/tickets?limit=50" \
    -H "Authorization: Bearer rfl_live_..."

  # Page 2 — paste nextCursor from page 1
  curl "https://api.replyful.com/v1/tickets?limit=50&startingAfter=<nextCursor>" \
    -H "Authorization: Bearer rfl_live_..."
  ```

  ```ts Node.js theme={null}
  async function listAllTickets(apiKey: string) {
    const all = [];
    let cursor: string | null = null;

    do {
      const url = new URL("https://api.replyful.com/v1/tickets");
      url.searchParams.set("limit", "100");
      if (cursor) {
        url.searchParams.set("startingAfter", cursor);
      }

      const res = await fetch(url, {
        headers: { Authorization: `Bearer ${apiKey}` },
      });
      if (!res.ok) {
        throw new Error(`Request failed: ${res.status}`);
      }

      const page = await res.json();
      all.push(...page.data);
      cursor = page.hasMore ? page.nextCursor : null;
    } while (cursor);

    return all;
  }
  ```

  ```python Python theme={null}
  import os, requests

  def list_all_tickets():
      headers = {"Authorization": f"Bearer {os.environ['REPLYFUL_API_KEY']}"}
      cursor = None
      items = []

      while True:
          params = {"limit": 100}
          if cursor:
              params["startingAfter"] = cursor

          res = requests.get(
              "https://api.replyful.com/v1/tickets",
              headers=headers,
              params=params,
              timeout=30,
          )
          res.raise_for_status()
          page = res.json()

          items.extend(page["data"])
          if not page["hasMore"]:
              break
          cursor = page["nextCursor"]

      return items
  ```
</CodeGroup>

## Rules of thumb

<Note>
  Cursors are opaque base64url strings whose internal layout is an implementation detail. Do not parse them, build them, or rely on their length — the format may change.
</Note>

* **Don't persist cursors long-term.** They are scoped to the sort order and filters of the request that produced them. A cursor from `?sort=-createdAt` is meaningless against `?sort=updatedAt`.
* **Hold filters constant while paging.** Changing `status`, `q`, or `sort` mid-loop will produce inconsistent pages.
* **Use `hasMore`, not `data.length`.** A page can return fewer items than `limit` and still have more pages waiting (e.g. when results have been filtered after fetching).
* **Hard cap at 100 per page.** Higher values are rejected with `422 invalid_query_parameter`.
