Skip to main content
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, 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.
  • Understanding of the filter structure (groups[].conditions[]) — see Pagination and 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.
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
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.

Picking the sync interval

Run the incremental sync on an interval driven by your customer’s tolerance for data staleness:
Webhooks are still the preferred mechanism for change detection — see 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.

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

By custom field value

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.

By contact type and state

Multiple conditions within a single group typically combine with AND logic. See Pagination and Filtering 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
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
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.
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.

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

Including archived contacts

By default, POST /api/Contact/Query excludes archived Contacts. Include them by setting includeArchived: true in the body:
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 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

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

Query Donations by Date Range

The Gift equivalent of this workflow — same pattern, different resource.

Sync External Donations into Virtuous

Use this query pattern as a reconciliation backstop in a full sync architecture.

Pagination and Filtering

The underlying mechanics of skip/take and the filter structure.

Webhooks Overview

The preferred primary signal for change detection — use this query pattern as a backstop.
Last modified on May 21, 2026