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

# Pagination & Filtering

> How cursor-based pagination, filtering, and sorting work across all Causeloop list endpoints.

All Causeloop list endpoints (`GET /v1/issues`, `GET /v1/patterns`, etc.) use **cursor-based pagination** and return a consistent response envelope. This page documents the common query parameters and the `page` object shape.

## Response envelope

Every list endpoint returns a `data` array and a `page` metadata object:

```json theme={null}
{
  "data": [ ... ],
  "page": {
    "next_cursor": "eyJvZmZzZXQiOiAyNX0",
    "has_more": true,
    "total": 142
  }
}
```

<ResponseField name="data" type="array">
  The current page of results.
</ResponseField>

<ResponseField name="page.next_cursor" type="string | null">
  An opaque cursor token. Pass it as `?cursor=` on the next request to retrieve the following page. `null` when you are on the last page.
</ResponseField>

<ResponseField name="page.has_more" type="boolean">
  `true` if there are more results after this page.
</ResponseField>

<ResponseField name="page.total" type="number">
  Total number of matching records across all pages. May be estimated for very large result sets (>10 000 records).
</ResponseField>

## Common query parameters

These parameters are supported on every list endpoint unless documented otherwise:

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

<ParamField query="cursor" type="string">
  Opaque cursor from the previous page's `page.next_cursor`. Omit for the first page.
</ParamField>

<ParamField query="sort" type="string">
  Sort field with optional direction prefix. Prefix with `-` for descending (default on most endpoints), no prefix for ascending. Example: `-created_at` (newest first), `created_at` (oldest first).
</ParamField>

<Note>
  Cursors are opaque and signed. Passing a modified or fabricated cursor string returns `400 Bad Request` with code `validation_error` and detail `"malformed cursor token"`.
</Note>

## Issues — additional filter params

<ParamField query="status" type="string">
  Filter by issue status. Values: `open`, `in_progress`, `resolved`, `closed`.
</ParamField>

<ParamField query="severity" type="string">
  Filter by severity. Values: `critical`, `high`, `medium`, `low`.
</ParamField>

<ParamField query="pattern_id" type="string">
  Filter issues belonging to a specific pattern.
</ParamField>

<ParamField query="q" type="string">
  Full-text search across issue title and description. Maximum 200 characters.
</ParamField>

<ParamField query="date_from" type="string">
  ISO-8601 UTC timestamp. Filters to issues with `source_created_at` on or after this instant — **inclusive** start of the range.
</ParamField>

<ParamField query="date_to" type="string">
  ISO-8601 UTC timestamp. Filters to issues with `source_created_at` strictly before this instant — **exclusive** end of the range. Together, `date_from`/`date_to` form a half-open `[date_from, date_to)` interval: a `date_to` value on a month boundary (e.g. the first of the next month) excludes that boundary instant itself.
</ParamField>

<ParamField query="has_pattern" type="boolean">
  Filter to issues that are (`true`) or are not (`false`) linked to a pattern.
</ParamField>

<Note>
  `date_to` on this endpoint is exclusive. This differs from `GET /settings/audit`, whose `date_to` remains **inclusive** — a known divergence between the two endpoints, not a bug.
</Note>

## Patterns — additional filter params

<ParamField query="status" type="string">
  Filter by pattern status. Values: `active`, `resolved`, `watching`.
</ParamField>

<ParamField query="q" type="string">
  Search pattern names. Maximum 200 characters.
</ParamField>

<ParamField query="domain" type="string">
  Filter patterns by domain fingerprint (e.g. `payments`, `auth`).
</ParamField>

<ParamField query="risk_min" type="integer">
  Minimum risk score (0–100, inclusive).
</ParamField>

<ParamField query="risk_max" type="integer">
  Maximum risk score (0–100, inclusive). Must be ≥ `risk_min`.
</ParamField>

**Allowed sort values for patterns:** `risk_score`, `-risk_score`, `issue_count`, `-issue_count`, `last_seen_at`, `-last_seen_at`.

## Paginating through results

Fetch page 1, then use the returned cursor for page 2:

<CodeGroup>
  ```bash curl theme={null}
  # Page 1
  curl "https://api.causeloop.ai/v1/issues?limit=25" \
    -H "Authorization: Bearer $CAUSELOOP_TOKEN"

  # Page 2 — use next_cursor from the previous response
  curl "https://api.causeloop.ai/v1/issues?limit=25&cursor=eyJvZmZzZXQiOiAyNX0" \
    -H "Authorization: Bearer $CAUSELOOP_TOKEN"
  ```

  ```javascript JavaScript theme={null}
  async function* paginateIssues(token, params = {}) {
    const base = 'https://api.causeloop.ai/v1/issues';
    let cursor = null;

    do {
      const url = new URL(base);
      Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
      if (cursor) url.searchParams.set('cursor', cursor);

      const resp = await fetch(url, {
        headers: { 'Authorization': `Bearer ${token}` },
      });
      const body = await resp.json();

      yield* body.data;

      cursor = body.page.has_more ? body.page.next_cursor : null;
    } while (cursor);
  }

  // Usage — iterate all open high-severity issues
  for await (const issue of paginateIssues(token, { status: 'open', severity: 'high' })) {
    console.log(issue.id, issue.title);
  }
  ```

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

  def paginate_issues(token, **params):
      url = 'https://api.causeloop.ai/v1/issues'
      cursor = None

      while True:
          query = {**params, 'limit': 25}
          if cursor:
              query['cursor'] = cursor

          resp = requests.get(
              url,
              params=query,
              headers={'Authorization': f'Bearer {token}'},
          )
          resp.raise_for_status()
          body = resp.json()

          yield from body['data']

          if not body['page']['has_more']:
              break
          cursor = body['page']['next_cursor']

  # Usage
  for issue in paginate_issues(access_token, status='open', severity='high'):
      print(issue['id'], issue['title'])
  ```
</CodeGroup>

## Filtering and sorting example

Fetch the 10 highest-risk active patterns in the `payments` domain:

<CodeGroup>
  ```bash curl theme={null}
  curl "https://api.causeloop.ai/v1/patterns?status=active&domain=payments&sort=-risk_score&limit=10" \
    -H "Authorization: Bearer $CAUSELOOP_TOKEN"
  ```

  ```javascript JavaScript theme={null}
  const resp = await fetch(
    'https://api.causeloop.ai/v1/patterns?' +
      new URLSearchParams({ status: 'active', domain: 'payments', sort: '-risk_score', limit: '10' }),
    { headers: { 'Authorization': `Bearer ${accessToken}` } },
  );
  const { data } = await resp.json();
  ```

  ```python Python theme={null}
  resp = requests.get(
      'https://api.causeloop.ai/v1/patterns',
      params={'status': 'active', 'domain': 'payments', 'sort': '-risk_score', 'limit': 10},
      headers={'Authorization': f'Bearer {access_token}'},
  )
  patterns = resp.json()['data']
  ```
</CodeGroup>
