> ## 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.

# Query Donors by Filters

> The workflow for reading Donors from Raise — the three lookup surfaces (search, list, query), common filter scenarios, and the iteration patterns for bulk reads and segmentation.

Reading Donor records is the foundation of most analytics, reporting, and sync workflows. Partner integrations query donors to build mailing lists, segment for stewardship, sync to external CRMs, and produce donor-level reports.

Raise exposes three distinct read surfaces for donors — `GET /api/Donor/search` for free-text lookups, `GET /api/Donor/list` for paginated lists with simple filters, and `POST /api/Donor/query` for structured filtering. This workflow covers when to use each.

For the resource-level reference of the Donor record, see [Donors](/raise/concepts/donors).

## When to use this workflow

| Scenario                                                  | Approach                                                                                        |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Type-ahead suggestions in a UI ("find donor named Bruce") | `GET /api/Donor/search`                                                                         |
| Quick "is this email already a donor?" check              | `GET /api/Donor/search` — see [Create or Find a Donor](/raise/workflows/create-or-find-a-donor) |
| Recent donors for a dashboard                             | `GET /api/Donor/list` with `Take` and a sort                                                    |
| Top donors by lifetime giving                             | `GET /api/Donor/list` with `SortBy=giftsum&Descending=true`                                     |
| Bulk export of donors matching specific criteria          | `POST /api/Donor/query` with structured conditions                                              |
| Donors with no recent activity (for re-engagement)        | `POST /api/Donor/query` with date conditions on `modifieddatetimeutc`                           |
| Streaming donors for downstream sync                      | `POST /api/Donor/query` with ID-cursor iteration                                                |

***

## Pattern 1: free-text search with `GET /api/Donor/search`

The simplest lookup. The endpoint accepts a single `filter` query parameter and matches across multiple donor fields:

> Search for donors by a filter string. Can match on donor ID, first name, last name, organization name, phone number, email address, postal code, and state. Supports searching by first/last name combinations, name with state, name with postal code, phone number in (xxx) xxx-xxxx format, and email in angle brackets.

```bash cURL theme={null}
curl "https://prod-api.raisedonors.com/api/Donor/search?filter=bruce@wayne.example" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

Supported filter formats:

| Input shape                                       | Matches                                   |
| ------------------------------------------------- | ----------------------------------------- |
| Bare email (`bruce@wayne.example`)                | Email field                               |
| Email in angle brackets (`<bruce@wayne.example>`) | Email field (exact match, less ambiguity) |
| Phone in `(xxx) xxx-xxxx` format                  | Phone field                               |
| First name only (`Bruce`)                         | First-name match                          |
| First and last name (`Bruce Wayne`)               | Combined name match                       |
| Name with state (`Bruce Wayne NJ`)                | Name plus state                           |
| Name with postal code (`Bruce Wayne 10001`)       | Name plus postal code                     |
| Bare donor ID (`12345`)                           | ID match                                  |

The endpoint is best for interactive lookups where the user can type a flexible search term. For deterministic exact-match queries, prefer the structured query in Pattern 3.

### When `search` returns multiple matches

Common scenarios:

* A name search where multiple donors share the name.
* An email search where a donor has the email on multiple records (typically a data-quality issue worth flagging).
* A postal-code search that hits many donors in the same area.

Handle the multi-match case explicitly in your integration:

```javascript JavaScript theme={null}
async function findDonorOrSurface(searchTerm) {
  const response = await fetch(
    `https://prod-api.raisedonors.com/api/Donor/search?filter=${encodeURIComponent(searchTerm)}`,
    { headers: { Authorization: `Bearer ${process.env.RAISE_API_TOKEN}` } }
  );
  const results = await response.json();

  if (results.length === 0) return { match: null, candidates: [] };
  if (results.length === 1) return { match: results[0], candidates: [] };

  // Multiple matches — surface for human disambiguation
  return { match: null, candidates: results };
}
```

For programmatic flows that need to act on a single result (e.g., "find this donor by email and update them"), use Pattern 3's exact-match query instead.

***

## Pattern 2: paginated list with `GET /api/Donor/list`

For ranked or sorted lists of donors. Parameters:

| Parameter        | Type    | Description                                                |
| ---------------- | ------- | ---------------------------------------------------------- |
| `Filter`         | string  | Free-text filter                                           |
| `Skip`           | integer | Records to skip                                            |
| `Take`           | integer | Records per page                                           |
| `SortBy`         | string  | One of the documented sort fields below (case-insensitive) |
| `Descending`     | boolean | Sort direction                                             |
| `IncludeDetails` | boolean | Include addresses, contact methods, etc.                   |

### Documented sort fields

The Raise spec documents these sort options for `GET /api/Donor/list`:

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

Notable choices:

| Sort field                      | Use                                                    |
| ------------------------------- | ------------------------------------------------------ |
| `giftsum`                       | Total lifetime giving — for "top donors" lists         |
| `gifts`                         | Count of gifts given — for "most engaged donors" lists |
| `createddatetime`               | Newest donors first                                    |
| `modifieddatetimeutc`           | Recently-updated donors — useful for incremental sync  |
| `id`                            | Stable iteration order — use for bulk reads            |
| `name`, `firstname`, `lastname` | Alphabetical for display                               |

### Top donors by lifetime giving

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

Returns the 25 donors with the highest total lifetime giving. Useful for major-donor cultivation dashboards.

### Recently-active donors

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

Returns donors with the most recent activity. Combined with a checkpoint, this is a viable lightweight sync pattern — see [incremental sync](#incremental-sync) below.

***

## Pattern 3: structured query with `POST /api/Donor/query`

For anything beyond free-text and basic sort. The body has the same shape as other `/query` endpoints (see [Pagination and Filtering: Pagination on /query endpoints](/raise/pagination#pagination-on-query-endpoints)):

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

<Warning>
  As with all Raise query endpoints, the `operator` integer values (0–29) and `queryType` integer values (0–9) are not labeled in the OpenAPI spec. Discover the valid integers via `GET /api/Query/options/{queryType}` at integration startup. See [Pagination and Filtering: Discovering query options](/raise/pagination#discovering-query-options).
</Warning>

### Common donor filter scenarios

#### Donors with no recent activity

```bash cURL theme={null}
curl -X POST https://prod-api.raisedonors.com/api/Donor/query \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "skip": 0,
    "take": 1000,
    "sortBy": "modifieddatetimeutc",
    "descending": false,
    "groups": [
      {
        "conditions": [
          { "parameter": "modifieddatetimeutc", "operator": 11, "value": "2024-07-01" }
        ]
      }
    ]
  }'
```

Returns donors whose `modifieddatetimeutc` is before mid-2024 — candidates for re-engagement campaigns. The `operator: 11` placeholder represents "less than" (discover the actual integer via QueryOptions).

#### High-value donors

```bash cURL theme={null}
curl -X POST https://prod-api.raisedonors.com/api/Donor/query \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "skip": 0,
    "take": 100,
    "sortBy": "giftsum",
    "descending": true,
    "groups": [
      {
        "conditions": [
          { "parameter": "giftsum", "operator": 10, "value": "10000" }
        ]
      }
    ]
  }'
```

Returns donors with lifetime giving above \$10,000, sorted by total descending. Useful for major-donor segmentation.

#### Donors by state (organization-wide segmentation)

```bash cURL theme={null}
curl -X POST https://prod-api.raisedonors.com/api/Donor/query \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "skip": 0,
    "take": 1000,
    "groups": [
      {
        "conditions": [
          { "parameter": "state", "operator": 0, "value": "NJ" }
        ]
      }
    ]
  }'
```

Returns donors with addresses in New Jersey. Useful for region-specific outreach.

#### Excluding archived and test-mode donors

For production reports, always filter out archived and test-mode donors:

```json theme={null}
{
  "skip": 0,
  "take": 1000,
  "groups": [
    {
      "conditions": [
        { "parameter": "isArchived", "operator": 0, "value": "false" },
        { "parameter": "isTestMode", "operator": 0, "value": "false" }
      ],
      "conjunct": 0
    }
  ]
}
```

A common pattern: build a helper function that wraps every query with these filters automatically:

```javascript JavaScript theme={null}
function productionDonorFilters(additionalConditions = []) {
  return [
    {
      conditions: [
        { parameter: 'isArchived', operator: EQUALS, value: 'false' },
        { parameter: 'isTestMode', operator: EQUALS, value: 'false' },
        ...additionalConditions,
      ],
      conjunct: AND_CONJUNCT,
    },
  ];
}
```

This makes it harder to accidentally include archived or test data in production-facing reports.

***

## Reducing payload size

For bulk reads where you don't need every field, `selectedColumns` reduces per-item payload:

```bash cURL theme={null}
curl -X POST https://prod-api.raisedonors.com/api/Donor/query \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "skip": 0,
    "take": 1000,
    "selectedColumns": ["id", "email", "name", "giftsum"],
    "groups": [
      {
        "conditions": [
          { "parameter": "giftsum", "operator": 10, "value": "100" }
        ]
      }
    ]
  }'
```

For a mailing-list export that only needs id, email, name, and lifetime giving, the smaller response is significantly faster than the full record.

The set of valid column names is discovered via `GET /api/Query/options/{queryType}`.

### `IncludeDetails` for full donor records

The opposite case: when you do need full records with addresses and contact methods, set `includeDetails: true`:

```json theme={null}
{
  "skip": 0,
  "take": 100,
  "includeDetails": true,
  "groups": [
    {
      "conditions": [
        { "parameter": "id", "operator": 0, "value": "12345" }
      ]
    }
  ]
}
```

This is much heavier per record — the spec calls out the performance impact 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.

Use `includeDetails: true` sparingly for bulk reads. For exporting one record at a time, `GET /api/Donor/{donorId}` is the cleaner path.

***

## Reading donor-related sub-resources

Several donor sub-resources have dedicated paginated endpoints — use these instead of filtered queries when the workflow is donor-scoped:

| Endpoint                                         | Returns                           |
| ------------------------------------------------ | --------------------------------- |
| `GET /api/Donor/{donorId}/gifts`                 | All Gifts by this donor           |
| `GET /api/Donor/{donorId}/recurringgifts`        | All RecurringGifts by this donor  |
| `GET /api/Donor/{donorId}/projects`              | Projects this donor has supported |
| `GET /api/Donor/{donorId}/donor-fields`          | Custom field values               |
| `GET /api/Donor/{donorId}/campaign-statistics`   | Per-campaign giving summary       |
| `GET /api/Donor/{donorId}/segment-statistics`    | Per-segment giving summary        |
| `GET /api/Donor/{donorId}/motivation-statistics` | Per-motivation giving summary     |

These are typically cheaper than equivalent filtered queries on `/api/Gift/query` because the filter is applied at the source rather than evaluated across the full gift table.

***

## Iteration patterns

### Standard skip/take loop

```javascript JavaScript theme={null}
async function exportDonorsMatching(filterGroups) {
  const allDonors = [];
  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({
          skip, take,
          sortBy: 'id',
          descending: false,
          groups: filterGroups,
        }),
      }
    );

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

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

  return allDonors;
}
```

### Incremental sync

For partner integrations that periodically pull updated donors since the last run:

```javascript JavaScript theme={null}
async function pullUpdatedDonorsSince(lastSyncIso) {
  return exportDonorsMatching([
    {
      conditions: [
        { parameter: 'modifieddatetimeutc', operator: GT_OPERATOR, value: lastSyncIso },
      ],
    },
  ]);
}

async function dailyDonorSync() {
  const lastSync = await checkpointStore.get('donor_sync_last_run');
  const updated = await pullUpdatedDonorsSince(lastSync);

  for (const donor of updated) {
    await downstreamSystem.upsertContact(donor);
  }

  // Advance the checkpoint
  const latestModified = updated[updated.length - 1]?.modifiedDateTime;
  if (latestModified) {
    await checkpointStore.set('donor_sync_last_run', latestModified);
  }
}
```

The same patterns apply as in [Query Gifts: incremental sync](/raise/workflows/query-gifts-by-filters#a-common-pattern-incremental-sync) — advance the checkpoint to the last successfully-processed record's timestamp, not to the current time.

### ID-cursor iteration for very large sets

For donor counts in the hundreds of thousands, the skip-based loop becomes inefficient. Use ID-cursor iteration:

```javascript JavaScript theme={null}
async function streamDonorsCursor(baseFilterGroups) {
  const allDonors = [];
  let cursorId = 0;
  const take = 1000;

  while (true) {
    const groupsWithCursor = [
      ...baseFilterGroups,
      {
        conditions: [
          { parameter: 'id', operator: GT_OPERATOR, 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({
          skip: 0,
          take,
          sortBy: 'id',
          descending: false,
          groups: groupsWithCursor,
        }),
      }
    );

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

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

  return allDonors;
}
```

***

## Combining lookups for richer reports

For reports that need both donor and gift detail (a donor-level summary with their recent giving), combine the donor query with per-donor follow-up calls:

```javascript JavaScript theme={null}
async function buildMajorDonorReport() {
  // 1. Get high-value donors with summary fields only
  const majorDonors = await exportDonorsMatching([
    {
      conditions: [
        { parameter: 'giftsum', operator: GT_OPERATOR, value: '10000' },
        { parameter: 'isArchived', operator: EQUALS, value: 'false' },
      ],
      conjunct: AND_CONJUNCT,
    },
  ]);

  // 2. For each, pull the recent gifts and the campaign statistics
  const enriched = await Promise.all(
    majorDonors.map(async (donor) => {
      const [gifts, campaignStats] = await Promise.all([
        fetchDonorGifts(donor.id, { take: 5 }),
        fetchDonorCampaignStatistics(donor.id),
      ]);
      return { donor, recentGifts: gifts, campaignStats };
    })
  );

  return enriched;
}
```

For large donor sets, throttle the per-donor follow-up calls — see [Rate Limits](/raise/rate-limits). For very large sets, batch the per-donor reads through a controlled-concurrency queue rather than `Promise.all`.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Query Gifts by Filters" icon="filter" href="/raise/workflows/query-gifts-by-filters">
    The Gift-side version of these patterns — useful for reports that combine both.
  </Card>

  <Card title="Donors" icon="user" href="/raise/concepts/donors">
    The Donor resource reference — fields, sub-resources, and special operations.
  </Card>

  <Card title="Create or Find a Donor" icon="user-plus" href="/raise/workflows/create-or-find-a-donor">
    The workflow that combines search, query, and the implicit creation path.
  </Card>

  <Card title="Pagination and Filtering" icon="list" href="/raise/pagination">
    The reference-level documentation of all three query patterns.
  </Card>
</CardGroup>
