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

# Pagination

> Walk Living Security Public API lists with the list envelope, the limit parameter, and opaque page_token cursors.

Every `/v1` list endpoint returns the same envelope and paginates the same
way: token-based, forward-only.

## The list envelope

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "id": "ent_8f3d2c1b0a9e",
      "object": "entity",
      "type": "person",
      "status": "active",
      "display_name": "Dana Rivera",
      "email": "dana.rivera@example.com",
      "created_at": "2025-11-03T09:21:44.000Z",
      "updated_at": "2026-07-28T16:02:10.000Z"
    }
  ],
  "next_page_token": "eyJvcmdhbml6YXRpb2...",
  "has_more": true
}
```

<ResponseField name="object" type="string">
  Always `"list"`.
</ResponseField>

<ResponseField name="data" type="array">
  The page of resources, in a stable, deterministic order.
</ResponseField>

<ResponseField name="next_page_token" type="string | null">
  Pass this as `page_token` on the next request to fetch the following page.
  `null` when there are no more results.
</ResponseField>

<ResponseField name="has_more" type="boolean">
  Whether more results exist beyond this page.
</ResponseField>

## Request parameters

<ParamField query="limit" type="integer" default="25">
  Page size. An integer between 1 and 500. Values outside that range return a
  `400` with code `invalid_request`.
</ParamField>

<ParamField query="page_token" type="string">
  The `next_page_token` from the previous response. Omit it for the first page.
</ParamField>

There are no offset parameters, no previous-page traversal, and no exact
total counts. To count a collection, walk it.

<Note>
  **Exception:** `GET /v1/cohorts/{id}/members` returns at most 50 rows per
  page even when you request a higher `limit`. `has_more` and
  `next_page_token` still reflect the true remainder of the cohort.
</Note>

Authenticate with either Basic credential shape — see
[Quickstart](/developers/v1/quickstart) (`apikey:secret` or `secret:`).
Set `BASE` to the organization's regional host — see
[Regions](/developers/v1/regions).

## Walking a list

Load `LIVING_SECURITY_API_KEY` from your secret manager as in the quickstart.
This example needs `entity:read`; the key used for the first-request guide may
need that additional grant. Install `requests` for Python or `jq` for the shell
example. Both examples pin a version, keep filters unchanged, and reject
redirects. They stop on errors and after 1,000 pages rather than reporting a
partial walk as complete. Python's read timeout bounds inactivity, not the total
job duration; impose a job deadline in your integration.

Request pages until `next_page_token` is `null`. Prefer `limit=500` for bulk
reads so you cover the collection in fewer requests — see
[rate limits](/developers/v1/rate-limits).

<CodeGroup>
  ```bash cURL + jq theme={null}
  set -euo pipefail
  # US: BASE="https://api.us-east-1.livingsecurity.ai/v1"
  BASE="https://api.eu-west-1.livingsecurity.ai/v1"
  : "${LIVING_SECURITY_API_KEY:?Set LIVING_SECURITY_API_KEY}"
  TOKEN=""

  # Bound each run. Increase this page budget deliberately for larger collections.
  for ((page_number=1; page_number<=1000; page_number++)); do
    PARAMS=(--data-urlencode "limit=500" --data-urlencode "type=person")
    if [ -n "$TOKEN" ]; then
      PARAMS+=(--data-urlencode "page_token=$TOKEN")
    fi
    RESULT=$(curl --silent --show-error --fail-with-body \
      --connect-timeout 5 --max-time 30 --get \
      -u "apikey:$LIVING_SECURITY_API_KEY" \
      -H "Living-Security-Version: 2026-07-01" \
      --write-out '\n%{http_code}' "${PARAMS[@]}" "$BASE/entities")
    STATUS="${RESULT##*$'\n'}"
    PAGE="${RESULT%$'\n'*}"
    [ "$STATUS" = "200" ] || { echo "Unexpected HTTP status: $STATUS" >&2; exit 1; }
    jq -e 'has("next_page_token") and .object == "list" and (.data | type == "array") and
      ((.has_more == false and .next_page_token == null) or
       (.has_more == true and (.next_page_token | type == "string" and length > 0)))' \
      <<< "$PAGE" > /dev/null
    NEXT=$(jq -r '.next_page_token // empty' <<< "$PAGE")
    [ -z "$NEXT" ] || [ "$NEXT" != "$TOKEN" ] || { echo "Repeated page token" >&2; exit 1; }
    jq -r '.data[].id' <<< "$PAGE"
    [ -n "$NEXT" ] || exit 0
    TOKEN="$NEXT"
  done
  echo "Page budget exhausted; the collection is incomplete" >&2
  exit 1
  ```

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

  # US: BASE = "https://api.us-east-1.livingsecurity.ai/v1"
  BASE = "https://api.eu-west-1.livingsecurity.ai/v1"
  AUTH = ("apikey", os.environ["LIVING_SECURITY_API_KEY"])

  def iter_entities(*, entity_type="person", max_pages=1000):
      if type(max_pages) is not int or max_pages < 1:
          raise ValueError("max_pages must be a positive integer")
      params = {"limit": 500, "type": entity_type}
      for _ in range(max_pages):
          response = requests.get(
              f"{BASE}/entities",
              auth=AUTH,
              params=params,
              headers={"Living-Security-Version": "2026-07-01"},
              timeout=(5, 30),
              allow_redirects=False,
          )
          try:
              response.raise_for_status()
              if response.status_code != 200:
                  raise RuntimeError("Unexpected response; check your regional API URL")
              page = response.json()
          finally:
              response.close()
          if (not isinstance(page, dict) or page.get("object") != "list"
                  or not isinstance(page.get("data"), list)
                  or type(page.get("has_more")) is not bool
                  or "next_page_token" not in page):
              raise RuntimeError("Invalid list response")
          token = page["next_page_token"]
          if ((page["has_more"] and (not isinstance(token, str) or not token))
                  or (not page["has_more"] and token is not None)):
              raise RuntimeError("Inconsistent list response")
          if token is not None and token == params.get("page_token"):
              raise RuntimeError("Repeated page token")
          yield from page["data"]
          if token is None:
              return
          params = {**params, "page_token": token}
      raise RuntimeError("Page budget exhausted; the collection is incomplete")

  if __name__ == "__main__":
      for entity in iter_entities():
          print(entity["id"])
  ```
</CodeGroup>

## Keep filters stable while paginating

Page tokens are opaque, signed cursors. Each token is bound to the
organization, the authenticated service account, the contract version, and the
exact filter set of the request that produced it. Changing any of those mid-walk — for
example, adding a `type` filter on page two — returns a `400` with code
`invalid_request` on the `page_token` parameter:

```json 400 Invalid request theme={null}
{
  "type": "https://docs.livingsecurity.com/developers/v1/errors/invalid_request",
  "title": "Invalid request",
  "status": 400,
  "detail": "page_token does not match this request's organization, credential, contract version, or filters.",
  "code": "invalid_request",
  "param": "page_token",
  "request_id": "req_...",
  "doc_url": "https://docs.livingsecurity.com/developers/v1/errors/invalid_request",
  "retryable": false
}
```

To change filters, start a fresh walk from the first page.

<Tip>
  Treat `page_token` as an opaque string: pass it back exactly as received.
  Do not decode, store long-term, or construct tokens — their internal format
  can change at any time without notice.
</Tip>

<Note>
  List endpoints reject query parameters they do not support with a `400`
  `invalid_request` naming the parameter. Check each endpoint's reference for
  its supported filters.
</Note>

## Handle an interrupted or changing collection

These examples do not automatically retry errors. For `429`, honor `Retry-After`
and follow the bounded [retry guidance](/developers/v1/rate-limits). Keep the
same region, service account, version, filters and last successfully processed
page token when retrying a read. Changing credentials does not grant access:
each request still checks current permissions.

A multi-page read is not a snapshot of the collection. Rows can change while
you traverse it. Process resources by their stable `id` and make downstream
writes idempotent; a restart can return a row again. If a token is rejected,
inspect the `page_token` error and correct the request before restarting from
page one. Do not silently discard previously processed data or mark an
interrupted export complete. For exact reporting totals, use the appropriate
reporting resource instead of treating a live list traversal as a snapshot.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.