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.
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.
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
- Sort by modification date ascending. Processing oldest-to-newest means that if the sync is interrupted partway through, you can resume from the highest
modifiedDateTimeUtcyou’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:| 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. |
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
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
Pattern 3: bulk export
For an initial backfill or a full-export reporting use case, retrieve every Contact:JavaScript
- Empty
groups[]returns every Contact. includeArchived: trueincludes archived records — important for completeness in an initial load.- Sort by
idis the most stable sort field for resumability. If you sort bymodifiedDateTimeUtcon a full export and records get modified mid-export, you can re-process the same record or miss others; sorting byid(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-processedid between batches. If the export is interrupted, resume from that ID rather than restarting:
JavaScript
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:
| 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. |
Including archived contacts
By default,POST /api/Contact/Query excludes archived Contacts. Include them by setting includeArchived: true in the body:
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=1000for 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=25ortake=50for interactive UIs. Smaller pages load quickly and avoid fetching records the user never sees.- Sort on indexed fields. Default sort orders are indexed. Custom
sortByvalues 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. |
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.