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

# Pagination

> How Raise list and query endpoints paginate, the two filter surfaces (simple Filter parameter and advanced Query endpoints), and the iteration patterns for bulk reads.

Raise paginates list and query responses with an `items`/`total` envelope and a `Skip`/`Take` cursor. It also exposes two separate filter surfaces: a simple `Filter` query parameter on `/list` endpoints, and an advanced query-builder pattern on `/query` endpoints. This page covers both, plus the iteration patterns for processing large result sets reliably.

The conventions are Raise-specific — they differ from CRM+'s pagination conventions in ways that partners coming from CRM+ need to know. Two of the most important differences are called out as you encounter them on this page.

## The response envelope

Every paginated response from Raise uses the same shape:

```json theme={null}
{
  "items": [
    {
      "id": 12345,
      "firstName": "Bruce",
      "lastName": "Wayne",
      "email": "bruce@wayne.example"
    }
  ],
  "total": 8421
}
```

Two fields:

* **`items`** — the array of records for the current page. Its length is at most the requested `Take`.
* **`total`** — the total count of records matching the query across all pages, not just the current page.

<Warning>
  This is `items`, not `list`. CRM+ paginated responses use `list`/`total`; Raise uses `items`/`total`. Code that expects one shape against the other will silently produce empty results.
</Warning>

## The two filter surfaces

Raise exposes two distinct mechanisms for filtering results, and they live on different endpoints:

| Endpoint pattern                                | Filter mechanism                                                                                                           | Use                                                     |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `/list` (GET) — e.g., `GET /api/Donor/list`     | Simple `Filter` query parameter (free-text) plus pagination params                                                         | Quick list views, free-text searches, common-case reads |
| `/query` (POST) — e.g., `POST /api/Donor/query` | Structured query builder in the JSON request body — supports field-level conditions, multi-value matching, AND/OR grouping | Complex segmentation, exports, reporting                |

Most Raise list-and-search workflows use one or the other — not both on the same call. Choose based on what the workflow actually needs.

***

## Pagination on `/list` endpoints

All `/list` endpoints accept the same set of query parameters. Below is the canonical example using `GET /api/Donor/list`:

| Parameter        | Type    | Description                                                                                                        |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `Skip`           | integer | The number of records to skip before returning results. Use for paging through results.                            |
| `Take`           | integer | The maximum number of records to return in this response.                                                          |
| `SortBy`         | string  | The field to sort by. Endpoint-specific — see [Sort fields per endpoint](#sort-fields-per-endpoint).               |
| `Descending`     | boolean | If `true`, sort in descending order. If `false` or omitted, sort ascending.                                        |
| `Filter`         | string  | A free-text filter that narrows the result set. The exact matching behavior is endpoint-specific.                  |
| `IncludeDetails` | boolean | If `true`, include related entities (addresses, contact methods, etc.) in each item. Performance cost — see below. |

<Warning>
  Query parameter names on `/list` endpoints use **PascalCase**: `Skip`, `Take`, `SortBy`, `Descending`, `Filter`, `IncludeDetails`. CRM+ uses `camelCase` (`skip`, `take`, etc.) on its equivalent endpoints. Sending the wrong casing produces unexpected behavior — the request may succeed but the parameter is ignored.

  Note also that the parameter names in the JSON body on `/query` endpoints use `camelCase` — see [Pagination on `/query` endpoints](#pagination-on-query-endpoints) below. This casing inconsistency between the two surfaces is a Raise-specific quirk; partners coming from CRM+ should not assume the same convention applies across endpoint types.
</Warning>

### A basic list request

```bash cURL theme={null}
curl -G https://prod-api.raisedonors.com/api/Donor/list \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode "Skip=0" \
  --data-urlencode "Take=100" \
  --data-urlencode "SortBy=createddatetime" \
  --data-urlencode "Descending=true"
```

The same request in JavaScript:

```javascript JavaScript theme={null}
const params = new URLSearchParams({
  Skip: '0',
  Take: '100',
  SortBy: 'createddatetime',
  Descending: 'true',
});

const response = await fetch(
  `https://prod-api.raisedonors.com/api/Donor/list?${params}`,
  { headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` } }
);

const page = await response.json();
console.log(`${page.items.length} of ${page.total} donors`);
```

### Sort fields per endpoint

The valid `SortBy` values are endpoint-specific. The Raise spec documents them in the parameter description. For `GET /api/Donor/list`, the sortable fields are:

```text theme={null}
createdate
createddatetime
email
firstname
gifts
giftsum
id
lastname
modifieddatetimeutc
name
phone
```

The values are **case-insensitive** but spelled in lowercase in the spec. Other `/list` endpoints (`/api/Gift/list`, `/api/Campaign/list`, etc.) have their own sortable-field sets — check the parameter description on the specific endpoint.

### The `Filter` parameter

The `Filter` query parameter is a free-text search filter on `/list` endpoints. The Raise OpenAPI spec does not document the exact matching behavior — whether it searches across all fields, specific fields, supports operators, or uses any wildcard syntax. Behavior may vary per endpoint.

<Note>
  The exact matching semantics of the `Filter` parameter are not documented in the spec. For predictable, structured filtering, prefer the `/query` endpoint pattern below. Use `Filter` for quick free-text searches in interactive contexts where the user can iterate on the search term.
</Note>

### The `IncludeDetails` toggle

Several `/list` endpoints support an `IncludeDetails` boolean parameter. When `true`, the response includes related entities embedded in each item — for `GET /api/Donor/list`, this means each donor's addresses and contact methods are included inline.

The Raise spec calls this out explicitly on `POST /api/Donor/query`:

> When `includeDetails=true`, the response includes all related entities (DonorAddresses, DonorContactMethods) similar to the GET by ID endpoint. This may impact performance for large result sets.

Two patterns:

| When `IncludeDetails` is true                                                 | When `IncludeDetails` is false                                    |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Each item carries full related-entity data — addresses, contact methods, etc. | Each item carries summary data only — `id`, `name`, `email`, etc. |
| Use for export workflows where the full record is needed                      | Use for list views, segment iteration, change detection           |
| Slower per request; less bulk throughput                                      | Faster per request; higher bulk throughput                        |

Default to `IncludeDetails=false` and only request full details when the workflow needs them.

### A complete pagination loop

The canonical pattern for iterating through every record matching a list query:

```javascript JavaScript theme={null}
async function listAllDonors() {
  const allDonors = [];
  let skip = 0;
  const take = 1000;       // Use the maximum supported value for bulk reads
  let total = null;

  do {
    const params = new URLSearchParams({
      Skip: skip.toString(),
      Take: take.toString(),
      SortBy: 'id',          // Stable sort for resumable iteration
      Descending: 'false',
    });

    const response = await fetch(
      `https://prod-api.raisedonors.com/api/Donor/list?${params}`,
      { headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` } }
    );

    if (!response.ok) {
      throw new Error(`Page ${skip} failed: ${response.status}`);
    }

    const page = await response.json();
    if (total === null) {
      total = page.total;
      console.log(`Iterating over ${total} donors`);
    }

    allDonors.push(...page.items);
    skip += take;
  } while (skip < total);

  return allDonors;
}
```

Three patterns that matter:

* **Loop condition is `skip < total`, not `page.items.length === take`.** The last page typically returns fewer items than `Take`.
* **`total` is captured from the first response.** Capturing it once gives stable bounds even if records are inserted concurrently.
* **Sort by `id` for stable iteration.** Other sort orders may produce inconsistent results if records change during pagination.

## Pagination on `/query` endpoints

The advanced query-builder pattern lives on `/query` endpoints — `POST /api/Donor/query`, `POST /api/Gift/query`, `POST /api/Campaign/query`, `POST /api/RecurringGift/query`, `POST /api/Segment/query`. The pagination and sorting parameters are present, but in a different form: they're **camelCase fields in the JSON body**, not query parameters.

```json theme={null}
{
  "skip": 0,
  "take": 100,
  "sortBy": "createddatetime",
  "descending": true,
  "includeDetails": false,
  "queryType": 0,
  "groups": [
    {
      "conditions": [
        {
          "parameter": "email",
          "operator": 0,
          "value": "@wayne.example"
        }
      ],
      "conjunct": 0
    }
  ]
}
```

<Warning>
  Body fields on `/query` endpoints use `camelCase` (`skip`, `take`, `sortBy`, `descending`, `includeDetails`). Query parameters on `/list` endpoints use `PascalCase` (`Skip`, `Take`, `SortBy`, `Descending`, `IncludeDetails`). The casing difference is real and the API enforces it.

  This is a Raise-specific quirk that is not present in CRM+ (which uses `camelCase` consistently). The platform team is aware of the inconsistency; reconciliation is expected in a future API version.
</Warning>

### The query body fields

| Field             | Type             | Description                                                                |
| ----------------- | ---------------- | -------------------------------------------------------------------------- |
| `skip`            | integer          | Records to skip — same semantics as `Skip` on `/list`                      |
| `take`            | integer          | Maximum records to return                                                  |
| `sortBy`          | string           | Sort field — same valid values as the corresponding `/list` endpoint       |
| `descending`      | boolean          | Sort direction                                                             |
| `includeDetails`  | boolean          | Include related entities — same semantics as `IncludeDetails` on `/list`   |
| `queryType`       | integer          | The type of query being executed. See [Query types](#query-types) below.   |
| `groups`          | array            | Array of `QueryGroupModel` — the filter structure                          |
| `selectedColumns` | array of strings | List of result columns to return for each item — lets you request a subset |
| `filter`          | string           | Free-text filter — same role as `Filter` on `/list`                        |

### The filter structure

The `groups[]` array carries the structured filter. Each group contains a set of `conditions[]` and a `conjunct` operator that determines how the conditions combine:

```json theme={null}
{
  "groups": [
    {
      "conditions": [
        {
          "parameter": "email",
          "operator": 0,
          "value": "@wayne.example"
        },
        {
          "parameter": "giftsum",
          "operator": 10,
          "value": "1000"
        }
      ],
      "conjunct": 0
    }
  ]
}
```

Three concepts:

* **`parameter`** is the field being filtered. The valid parameter names per query type are discovered via `GET /api/Query/options/{queryType}` — see [Discovering query options](#discovering-query-options).
* **`operator`** is an integer enum (`0–29`, 30 possible values). The numeric values map to operators like Equals, Not Equals, Greater Than, Between, etc. — but the spec does not expose the mapping.
* **`conjunct`** is an integer enum (`0` or `1`) that combines conditions or groups. The two values correspond to AND and OR, but the spec does not document which is which.

A condition can also use `values` (array) for multi-value matches or `secondaryValue` for two-bound operators (Between).

<Warning>
  The `operator` and `conjunct` values are integer enums in the spec without label documentation. Partner integrations must discover the integer-to-meaning mapping through the `GET /api/Query/options/{queryType}` endpoint (or empirically by testing). Building queries with hardcoded integer literals is fragile until the mapping is published.

  **⚠️ Spec gap:** The mapping from `QueryOperator` integers (0–29) to semantic operators (Equals, Greater Than, Contains, etc.) is not documented in the OpenAPI spec. Future spec updates are expected to label these enum values. Until then, use the discovery endpoint at integration startup and store the mapping in your integration.
</Warning>

### Query types

The `queryType` field on a query request identifies what's being queried — donor, gift, recurring gift, campaign, etc. Like `operator`, it's an integer enum (`0–9`) without documented labels in the spec.

When calling a specific query endpoint (`POST /api/Donor/query`), the `queryType` is implied — the endpoint operates on donors. The field is still required in the body, with the integer value matching the donor query type.

The `GET /api/Query/list/{queryType}` and `GET /api/Query/options/{queryType}` endpoints take the `queryType` integer as a path parameter, so partners do need to know the integer values to use them.

### Discovering query options

The `GET /api/Query/options/{queryType}` endpoint returns the available parameters, operators, and result fields for a given query type:

```bash cURL theme={null}
curl https://prod-api.raisedonors.com/api/Query/options/0 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

The response shape is not formally documented in the spec (the response schema is empty). In practice, partners should use this endpoint at integration startup to discover:

* Valid `parameter` names for filter conditions
* The integer-to-label mapping for operators
* The list of selectable result columns (for `selectedColumns`)

Cache the discovery result and refresh it on a slow cadence (daily is fine).

### Selecting specific columns

The `selectedColumns` array on a query request lets you request a subset of result fields rather than the full record. For large result sets where the full record is unnecessary, this reduces response payload size:

```json theme={null}
{
  "queryType": 0,
  "skip": 0,
  "take": 1000,
  "selectedColumns": ["id", "email", "giftsum"],
  "groups": [
    {
      "conditions": [{ "parameter": "giftsum", "operator": 10, "value": "1000" }]
    }
  ]
}
```

The set of valid column names is discovered via `GET /api/Query/options/{queryType}` (same endpoint as for parameters and operators).

## Iteration patterns

### Basic pagination

The same pattern as for `/list` endpoints, applied to the body fields:

```javascript JavaScript theme={null}
async function queryAllDonors(filterGroups, queryType) {
  const allItems = [];
  let skip = 0;
  const take = 1000;
  let total = null;

  do {
    const response = await fetch(
      'https://prod-api.raisedonors.com/api/Donor/query',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          queryType,
          skip,
          take,
          sortBy: 'id',
          descending: false,
          groups: filterGroups,
        }),
      }
    );

    const page = await response.json();
    if (total === null) total = page.total;

    allItems.push(...page.items);
    skip += take;
  } while (skip < total);

  return allItems;
}
```

### ID-cursor iteration for very large result sets

For result sets in the tens of thousands of records or larger, the `skip`-based loop has two costs: server-side performance degrades at high `skip` values, and concurrent inserts can shift offsets. An ID-cursor pattern avoids both:

```javascript JavaScript theme={null}
async function queryAllDonorsCursor(baseFilterGroups, queryType, idOperator) {
  // idOperator: the integer enum value for "Greater Than" — discover via /api/Query/options
  const allItems = [];
  let cursorId = 0;
  const take = 1000;

  while (true) {
    // Add an ID > cursorId condition to each request
    const groupsWithCursor = [
      ...baseFilterGroups,
      {
        conditions: [
          { parameter: 'id', operator: idOperator, value: cursorId.toString() },
        ],
      },
    ];

    const response = await fetch(
      'https://prod-api.raisedonors.com/api/Donor/query',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          queryType,
          skip: 0,
          take,
          sortBy: 'id',
          descending: false,
          groups: groupsWithCursor,
        }),
      }
    );

    const page = await response.json();
    if (page.items.length === 0) break;

    allItems.push(...page.items);
    cursorId = page.items[page.items.length - 1].id;
  }

  return allItems;
}
```

The cursor approach requires knowing the integer operator value for "Greater Than" — which you discover via `GET /api/Query/options/{queryType}` at integration startup.

## Performance considerations

### Use the largest `Take` the endpoint supports

Each request consumes one slot in your rate-limit budget regardless of how many records it returns. A `Take=1000` request returns 1,000 records for the cost of one request; a `Take=25` request returns 25. For bulk operations, use the maximum the endpoint accepts.

For interactive list views in a UI, smaller pages (`Take=25` or `Take=50`) load faster and don't fetch records the user never sees.

### Filter aggressively in the request, not client-side

A query that returns 500 records after filtering is much cheaper than one that returns 50,000 you discard client-side. Push filters into the request — either as `Filter` on `/list` or as `groups[]` on `/query` — wherever possible.

### Use `IncludeDetails=false` when you can

Embedded related entities (`includeDetails: true`) significantly increases response payload size. Use it only when the workflow needs the full record. For change detection, segment iteration, and list views, the default summary representation is enough.

### Use `selectedColumns` on `/query` endpoints

For workflows that only need a handful of fields per record, `selectedColumns` reduces payload size without sacrificing pagination throughput.

## Where to go next

<CardGroup cols={2}>
  <Card title="Rate Limits" icon="gauge" href="/raise/rate-limits">
    The throttling patterns that pair with pagination strategy.
  </Card>

  <Card title="The Raise Data Model" icon="diagram-project" href="/raise/concepts/data-model">
    Understand what `parameter` values mean per query type.
  </Card>

  <Card title="Query Gifts by Filters" icon="filter" href="/raise/workflows/query-gifts-by-filters">
    The Gift-specific version of the query patterns on this page.
  </Card>

  <Card title="Query Donors by Filters" icon="filter" href="/raise/workflows/query-donors-by-filters">
    The Donor-specific version of the query patterns on this page.
  </Card>
</CardGroup>
