> ## 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 Contacts by Filters

> Retrieve Contact records by structured filter criteria — incremental sync by modification date, tag-based segmentation queries, and paginated bulk export.

`POST /api/Contact/Query` is the primary endpoint for retrieving Contact records by criteria other than ID. This workflow walks through three of the most common scenarios for partner integrations: incremental sync by modification date, tag-based or custom-field-based queries, and bulk export of large result sets.

If you have not read [Pagination and Filtering](/crm/pagination), start there — this page builds on the structured-filter pattern introduced there.

## Scenario

Your integration needs to retrieve Contacts that match some criteria. The most common cases for partners:

* **Incremental sync.** Find Contacts modified since your last sync run to keep your local copy current.
* **Targeted retrieval.** Find Contacts matching a specific tag, custom field value, or attribute for a specific workflow (e.g., everyone tagged "Major Donor" for a major-gift campaign).
* **Bulk export.** Retrieve every Contact for an initial data load or reporting export.

All three use the same endpoint and pattern — they differ in the filter structure and the loop strategy.

## Prerequisites

* A valid CRM+ API token — see [Authentication](/crm/authentication).
* Understanding of the filter structure (`groups[].conditions[]`) — see [Pagination and Filtering](/crm/pagination#part-2-filtering).
* The list of valid filter parameters and operators for the customer's organization, retrieved from `GET /api/Contact/QueryOptions`.

***

## Step 1: discover valid filter parameters

Before constructing filters, retrieve the parameters and operators valid for the current organization. `Contact Type`, `Last Modified Date`, and similar parameters exist in most organizations but the exact set — including custom-field-derived parameters — varies.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.virtuoussoftware.com/api/Contact/QueryOptions \
    -H "Authorization: Bearer YOUR_API_TOKEN"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    'https://api.virtuoussoftware.com/api/Contact/QueryOptions',
    {
      headers: { Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}` },
    }
  );

  const queryOptions = await response.json();
  const parameters = queryOptions.options.map((o) => o.parameter);
  console.log('Available filter parameters:', parameters);
  ```
</CodeGroup>

Cache the result at integration startup. The available parameters change rarely; refresh daily at most.

***

## Pattern 1: incremental sync by modification date

The most common use of Contact Query in a partner integration: find Contacts modified since the last sync run.

```javascript JavaScript theme={null}
async function pullModifiedContacts(lastSyncTimestamp) {
  const allContacts = [];
  let skip = 0;
  const take = 1000;
  let total = null;
  let highestModifiedDate = lastSyncTimestamp;

  do {
    const response = await fetch(
      'https://api.virtuoussoftware.com/api/Contact/Query',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          groups: [
            {
              conditions: [
                {
                  parameter: 'Last Modified Date',
                  operator: 'Is After',
                  value: lastSyncTimestamp,
                },
              ],
            },
          ],
          sortBy: 'last modified date',
          descending: false,           // oldest first — process in order
          skip,
          take,
        }),
      }
    );

    if (!response.ok) {
      throw new Error(`Query failed: ${response.status}`);
    }
    const page = await response.json();

    if (total === null) {
      total = page.total;
      console.log(`Found ${total} modified contacts since ${lastSyncTimestamp}`);
    }

    for (const contact of page.list) {
      allContacts.push(contact);
      if (contact.modifiedDateTimeUtc > highestModifiedDate) {
        highestModifiedDate = contact.modifiedDateTimeUtc;
      }
    }

    skip += take;
  } while (skip < total);

  return { contacts: allContacts, nextSyncTimestamp: highestModifiedDate };
}
```

Three patterns this gets right:

* **Sort by modification date ascending.** Processing oldest-to-newest means that if the sync is interrupted partway through, you can resume from the highest `modifiedDateTimeUtc` you've fully processed.
* **Track the highest modification date observed.** Use this as the floor for the next sync run, not the wall-clock time when you started the run. This avoids a race condition where records modified during the sync would be missed.
* **Use `take=1000`.** Large page sizes minimize round trips and rate-limit budget consumption. See [Rate Limits](/crm/rate-limits#staying-within-limits).

### Picking the sync interval

Run the incremental sync on an interval driven by your customer's tolerance for data staleness:

| Interval               | Use case                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------- |
| **Every 5–10 minutes** | Near-real-time integrations. Combine with webhooks for true real-time; use polling as a backstop. |
| **Hourly**             | Most partner integrations. Catches the typical rate of donor data changes without excess load.    |
| **Daily**              | Reporting integrations, data warehouses, or any consumer with daily-batch downstream processing.  |

<Tip>
  Webhooks are still the preferred mechanism for change detection — see [Webhooks Overview](/crm/webhooks/overview). Use this incremental query pattern as a reconciliation backstop, not as the primary signal. Run the query on a slower cadence than your webhook handler processes events.
</Tip>

***

## Pattern 2: targeted retrieval by tag or custom field

Find Contacts matching a specific tag, custom field value, or attribute. Useful for partner workflows that target specific donor segments.

### By tag

```json theme={null}
{
  "groups": [
    {
      "conditions": [
        { "parameter": "Tag", "operator": "Is", "value": "Major Donor" }
      ]
    }
  ],
  "sortBy": "name",
  "skip": 0,
  "take": 100
}
```

### By custom field value

```json theme={null}
{
  "groups": [
    {
      "conditions": [
        { "parameter": "Newsletter Subscriber", "operator": "Is", "value": "true" }
      ]
    }
  ],
  "sortBy": "name",
  "skip": 0,
  "take": 100
}
```

The exact `parameter` value for a custom field depends on the field's name configured in the organization. Discover via `GET /api/Contact/QueryOptions` — see [Pagination and Filtering](/crm/pagination#discover-available-filter-fields).

### By contact type and state

```json theme={null}
{
  "groups": [
    {
      "conditions": [
        { "parameter": "Contact Type", "operator": "Is", "value": "Household" },
        { "parameter": "State", "operator": "Is", "value": "AZ" }
      ]
    }
  ],
  "sortBy": "name",
  "skip": 0,
  "take": 100
}
```

Multiple conditions within a single group typically combine with AND logic. See [Pagination and Filtering](/crm/pagination#filter-structure) for the structure and the flagged human-input on group/condition combination logic.

***

## Pattern 3: bulk export

For an initial backfill or a full-export reporting use case, retrieve every Contact:

```javascript JavaScript theme={null}
async function exportAllContacts() {
  const allContacts = [];
  let skip = 0;
  const take = 1000;
  let total = null;

  do {
    const response = await fetch(
      'https://api.virtuoussoftware.com/api/Contact/Query',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          groups: [],                            // empty groups = all Contacts
          includeArchived: true,                 // include archived for completeness
          sortBy: 'id',                          // stable sort for resumability
          descending: false,
          skip,
          take,
        }),
      }
    );

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

    allContacts.push(...page.list);
    skip += take;

    console.log(`Exported ${allContacts.length} / ${total}`);
  } while (skip < total);

  return allContacts;
}
```

Three differences from the incremental sync:

* **Empty `groups[]`** returns every Contact.
* **`includeArchived: true`** includes archived records — important for completeness in an initial load.
* **Sort by `id`** is the most stable sort field for resumability. If you sort by `modifiedDateTimeUtc` on a full export and records get modified mid-export, you can re-process the same record or miss others; sorting by `id` (an immutable field) is safer for full exports.

### Resumable exports

For very large exports (hundreds of thousands of records) that may take hours, persist the last successfully-processed `id` between batches. If the export is interrupted, resume from that ID rather than restarting:

```javascript JavaScript theme={null}
async function exportAllContactsResumable(resumeFromId = 0) {
  let skip = 0;
  const take = 1000;

  while (true) {
    const response = await fetch(
      'https://api.virtuoussoftware.com/api/Contact/Query',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          groups: [
            {
              conditions: [
                { parameter: 'Contact Id', operator: 'Greater Than', value: resumeFromId.toString() },
              ],
            },
          ],
          sortBy: 'id',
          descending: false,
          skip,
          take,
        }),
      }
    );

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

    for (const contact of page.list) {
      await persistContact(contact);
      resumeFromId = contact.id;             // checkpoint after each record
    }

    skip += take;
  }
}
```

Replacing the `skip`-based loop with an ID-cursor-based loop also avoids the `skip` overhead — at high `skip` values, server-side query performance can degrade. ID-cursor pagination keeps each query bounded to a fresh range.

<Note>
  Confirm the exact parameter name (`Contact Id` vs. `Id` vs. `ContactId`) by calling `GET /api/Contact/QueryOptions`. The value used above is illustrative; the live API's enum is the authoritative source.
</Note>

***

## Working with the response

`POST /api/Contact/Query` returns an abbreviated Contact representation in the `list` array — `id`, `name`, `contactType`, `contactName`, `address`, `email`, `phone`, and `contactViewUrl`. For most sync workflows this is enough; the email and phone in the result are sufficient to surface in your platform's UI and to identify the donor for downstream processing.

If you need the full Contact record (with ContactIndividuals, all addresses, custom fields, contactReferences, etc.), there are two options:

| Approach                                                                    | Use                                                                                                                                                         |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Iterate the abbreviated results and `GET /api/Contact/{contactId}` for each | When you need full detail for a small subset (e.g., the contacts you intend to write back to).                                                              |
| Use `POST /api/Contact/Query/FullContact` instead                           | When you need full detail for the whole result set. Returns the same envelope but with complete Contact records. Slower per-request — use only when needed. |

<Warning>
  `POST /api/Contact/Query/FullContact` is meaningfully slower than the abbreviated variant. Use it only when your workflow actually needs the full payload — most incremental syncs do not. The abbreviated `POST /api/Contact/Query` plus a targeted full-detail fetch for the small subset you act on is usually more efficient.
</Warning>

***

## Including archived contacts

By default, `POST /api/Contact/Query` excludes archived Contacts. Include them by setting `includeArchived: true` in the body:

```json theme={null}
{
  "groups": [
    { "conditions": [{ "parameter": "Last Modified Date", "operator": "Is After", "value": "2024-12-01T00:00:00Z" }] }
  ],
  "includeArchived": true,
  "sortBy": "last modified date",
  "skip": 0,
  "take": 1000
}
```

For incremental sync, this is important — when a Contact is archived in Virtuous, the archive operation updates `modifiedDateTimeUtc`, but the record is excluded from default queries. Without `includeArchived: true`, your sync sees the archived Contact as "missing" rather than detecting the archive event.

See [Statuses and Lifecycle States](/crm/concepts/statuses-and-lifecycle-states#contact-lifecycle) for the broader treatment.

***

## Performance considerations

* **`take=1000` for bulk operations.** Each request returns up to 1,000 Contacts and consumes one rate-limit slot. At maximum throughput, you can retrieve 1.5 million Contacts per hour — well above the size of any typical nonprofit's donor database.
* **`take=25` or `take=50` for interactive UIs.** Smaller pages load quickly and avoid fetching records the user never sees.
* **Sort on indexed fields.** Default sort orders are indexed. Custom `sortBy` values may be slow on large result sets.
* **Filter aggressively.** A query that returns 500 Contacts after filtering is much cheaper than a query that returns 50,000 you discard client-side. Push filters into the request body wherever possible.

***

## Error handling

| Status                  | Cause                                | Action                                                                            |
| ----------------------- | ------------------------------------ | --------------------------------------------------------------------------------- |
| `400 Bad Request`       | Invalid filter parameter or operator | Confirm with `GET /api/Contact/QueryOptions`; re-check casing of parameter names. |
| `400 Bad Request`       | `take` exceeds 1000                  | Reduce `take` to 1000 or fewer.                                                   |
| `401 Unauthorized`      | Invalid token                        | Refresh credentials.                                                              |
| `403 Forbidden`         | API key permissions insufficient     | Verify the permission group.                                                      |
| `429 Too Many Requests` | Rate limit exceeded                  | Back off per `Retry-After`.                                                       |

Bulk queries are the most rate-limit-sensitive class of request. A misconfigured loop that paginates without checking termination conditions can burn the full hourly budget in minutes. Always assert that the loop is making progress (the `skip` value increases or the result set is finite).

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Query Donations by Date Range" icon="calendar" href="/crm/workflows/query-donations-by-date-range">
    The Gift equivalent of this workflow — same pattern, different resource.
  </Card>

  <Card title="Sync External Donations into Virtuous" icon="arrow-right-arrow-left" href="/crm/workflows/sync-external-donations">
    Use this query pattern as a reconciliation backstop in a full sync architecture.
  </Card>

  <Card title="Pagination and Filtering" icon="list" href="/crm/pagination">
    The underlying mechanics of `skip`/`take` and the filter structure.
  </Card>

  <Card title="Webhooks Overview" icon="bell" href="/crm/webhooks/overview">
    The preferred primary signal for change detection — use this query pattern as a backstop.
  </Card>
</CardGroup>
