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

# Pagination

> Cursor-based pagination for Operata list endpoints — the response envelope, `next_cursor` semantics, and a complete loop example.

List endpoints return one page at a time. Walk the full result set by following the opaque `next_cursor` value from each response until the server returns `null`. The cursor is the only state you carry between pages.

## Endpoint

```http theme={null}
GET https://api.operata.io/v1/data/calls
GET https://api.operata.io/v1/data/ccpLogs
```

Any endpoint that returns a list uses this envelope.

## Parameters

### Query

| Name       | Type              | Required           | Description                                                             |
| ---------- | ----------------- | ------------------ | ----------------------------------------------------------------------- |
| `cursor`   | string            | no                 | Opaque cursor returned by the previous page. Omit on the first request. |
| `size`     | integer           | no                 | Page size. 1–100. Default 50.                                           |
| `fromTime` | string (ISO 8601) | endpoint-dependent | Inclusive lower bound for time-windowed lists. UTC.                     |
| `toTime`   | string (ISO 8601) | endpoint-dependent | Exclusive upper bound for time-windowed lists. UTC.                     |

## Response

Every list response uses the same JSON envelope:

```json theme={null}
{
  "data": [
    { "id": "ctr_0HXYZ123456789ABCDE", "started_at": "2026-05-18T10:14:22Z" }
  ],
  "next_cursor": "eyJvZmZzZXQiOjUwfQ=="
}
```

| Field         | Type           | Description                                                        |
| ------------- | -------------- | ------------------------------------------------------------------ |
| `data`        | array          | The page of records. Length is at most `size`.                     |
| `next_cursor` | string \| null | Cursor for the next page, or `null` when you have reached the end. |

The cursor is opaque — don't parse it, base64-decode it, or assume it encodes an offset. Treat it as a string token the server issues and you echo back unchanged.

## Example

A loop that fetches every contact in a 24-hour window:

<CodeGroup>
  ```bash curl theme={null}
  # First page.
  curl -u "$OPERATA_GROUP_ID:$OPERATA_API_TOKEN" \
    "https://api.operata.io/v1/data/calls?fromTime=2026-05-17T00:00:00Z&toTime=2026-05-18T00:00:00Z&size=50"

  # Next page — copy the `next_cursor` from the previous response.
  curl -u "$OPERATA_GROUP_ID:$OPERATA_API_TOKEN" \
    "https://api.operata.io/v1/data/calls?cursor=eyJvZmZzZXQiOjUwfQ%3D%3D&size=50"
  ```

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

  def iter_contacts(from_iso, to_iso, size=50):
      cursor = None
      while True:
          params = {"fromTime": from_iso, "toTime": to_iso, "size": size}
          if cursor:
              params["cursor"] = cursor
          response = requests.get(
              "https://api.operata.io/v1/data/calls",
              auth=(os.environ["OPERATA_GROUP_ID"], os.environ["OPERATA_API_TOKEN"]),
              params=params,
          )
          response.raise_for_status()
          body = response.json()
          for record in body["data"]:
              yield record
          cursor = body.get("next_cursor")
          if cursor is None:
              return

  for contact in iter_contacts("2026-05-17T00:00:00Z", "2026-05-18T00:00:00Z"):
      print(contact["id"])
  ```
</CodeGroup>

## Errors

| Status | Code             | When                                                                                           | Recover                                                                             |
| ------ | ---------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| 400    | `invalid_cursor` | The cursor is malformed or was issued by a different query (different time window or filters). | Restart the walk from the first page with the same filters. Do not edit the cursor. |
| 400    | `invalid_range`  | `toTime` is missing, equal to, or before `fromTime`.                                           | Send a valid window.                                                                |

See [Errors](/docs/api-errors) for shared codes such as `401 unauthorized` and `429 rate_limited`.
