Advanced Search Overview
Advanced search powers the POST /search/{domain}/results endpoints. Every request has the same two-part body:
{
"configuration": { },
"filters": { }
}filters— what to match. A tree of constraints and groups.configuration— what to return, and which slice of the population to search.
Pagination is passed as query string parameters (?page=2&per_page=100), not inside the body.
Filters
A filter is a tree, where each node is either a constraint or a group.
Constraint node
A constraint applies one operator to one field.
{
"type": "constraint",
"id": "first_name",
"operator": "equal",
"value": "Tom",
"invert": false
}| Field | Type | Required | Description |
|---|---|---|---|
type | "constraint" | yes | Node discriminator. |
id | string | yes | The field to search. See the individual search params for the full list. |
operator | string | yes | Must be valid for the field's data type — see Data Types and Operators. |
value | mixed | depends | Shape depends on the data type and operator. Omit for is_set / is_not_set. |
invert | boolean | no | Negates the constraint. Defaults to false. |
The key isinvert, notinvertedA misspelled key is silently ignored — the constraint runs un-negated and you get the opposite result set with no error. This applies to group nodes too.
Group node
A group combines child nodes with a boolean operator. Groups nest to any depth.
{
"type": "group",
"operator": "and",
"invert": false,
"conditions": []
}| Field | Type | Required | Description |
|---|---|---|---|
type | "group" | yes | Node discriminator. |
operator | "and" | "or" | yes | How the children combine. |
conditions | array of nodes | yes | Constraint nodes, group nodes, or a mix. |
invert | boolean | no | Negates the whole group. Defaults to false. |
A bare constraint is valid at the root — you do not need to wrap a single condition in a group.
Configuration
configuration controls the result shape and the population searched. Every key is optional. Unrecognized keys are ignored.
Result shaping
| Key | Type | Default | Description |
|---|---|---|---|
return_search_results | boolean | true | Whether matching records are returned. Set to false when you only want return_family_positions output, or only the total count from the pagination headers. |
return_family_positions | array | [] | Also return family members of matched individuals, by position. Any of PRIMARY_CONTACT, SPOUSE, CHILD, OTHER. |
order_by | array | [] | Sort columns in precedence order. See below. |
columns | array of string | [] | Columns to include in the output. Only used by the export action — ignored by /results. |
search_id | integer | — | Run a saved search. See Running a saved search. |
order_by
order_byEach entry is an object:
{
"order_by": [
{ "column": "last_name", "direction": "asc" },
{ "column": "first_name", "direction": "asc" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
column | string | yes | A sortable column for the domain. Unknown columns are rejected. |
direction | "asc" | "desc" | no | Case-insensitive. Defaults to asc. |
Always setorder_bywhen pagingWithout an explicit sort, row order across pages is not guaranteed, so records can repeat or be skipped as you page through a large result set.
Population selection
These keys decide who is eligible to match, independently of your filters.
| Key | Type | Default | Description |
|---|---|---|---|
filter_profile_type | array | ["active"] | Which profile types to include. Any of active, inactive, deceased. |
include_inactive | boolean | false | Legacy alternative to adding inactive to filter_profile_type. Either mechanism works; they are OR'd together. |
include_deceased | boolean | false | Legacy alternative to adding deceased to filter_profile_type. |
include_lau | boolean | true | Include limited access users. Set false to exclude them. |
include_pending | boolean | true | Include pending (unapproved) profiles. Set false to exclude them. |
include_unlisted | boolean | permission-derived | Include unlisted individuals. See the note below. |
exclude_children | boolean | false | Exclude individuals below the organization's child-work age. |
The default population is active profiles onlyIf you omit
filter_profile_type, inactive and deceased individuals are excluded no matter what your filters say. Reporting integrations that expect a full population must set it explicitly.
Pending and limited access profiles are included by default
include_pendingandinclude_lauboth default totrue. A plain individuals search already returns pending profiles mixed in with approved ones. To report on approval state, filter on theapproval_statusfield explicitly rather than relying on these switches.
Two filters are applied automatically and cannot be turned off:
- Deceased-date constraints force deceased profiles in. If your filter tree contains a
date_deceased_partsordate_deceased_monthconstraint, deceased profiles are included regardless offilter_profile_type. - An
is_inactiveconstraint forces inactive profiles in. Same behavior.
include_unlisted is permission-governed
include_unlisted is permission-governedYou can always turn unlisted profiles off, but you cannot turn them on without permission:
- Omitted, or set to a truthy value → the API computes the value from the caller's permissions. You get unlisted individuals only in campuses where the caller may view them, and only listed individuals elsewhere.
- Set to
false→ respected. Unlisted individuals are excluded.
Server-controlled keys
These appear in the configuration object but are set by the API. Any value you send is overwritten or ignored — do not include them.
| Key | Why |
|---|---|
unlisted_campus_ids | The campuses where the caller may view unlisted individuals. Always overwritten from the caller's permissions. |
child_work_approved_campus_ids | Derived from the caller's child-work approvals. |
is_saved_search_collaborator | Set internally while resolving search_id. |
client_locale | Derived from the request. |
return_ids | Set internally by search actions. |
check_permissions | Derived from the caller. Scheduling domains only. |
Other domains
Some keys apply only to non-individual search domains and are ignored by /search/individuals/results:
group_by and include_active (event attendance) · limit_campus_ids, limit_category_ids, limit_team_ids, limit_schedule_ids, non_archived_categories_only (scheduling) · include_campuses (scheduling categories) · location, public_only, restrictive_mode, context_individual_id.
Pagination
Pagination is passed on the query string, not in the body.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | 1-based page number. |
per_page | integer | 25 | Page size. One of 25, 50, 75, 100. |
exclude_total | boolean | false | Skip computing the total row count. Faster on large result sets, but the total and page-count response headers become unreliable. |
Response headers: X-Page, X-Per-Page, X-Offset, X-Total-Pages.
exclude_totalis a query parameter, not a configuration key. Placing it inconfigurationhas no effect.
Data Types and Operators
Each searchable field has a data type. The valid operators — and the shape of value — depend on that type.
Shared value shapes
Range object — used by between and not_between:
{ "lbound": "2026-01-01", "ubound": "2026-03-31" }lbound and ubound are both dates, both numbers, or both times, matching the field's type.
Relative range object — used by the in_the_* operator families:
{ "period": "month", "units": 3, "anniversary": false }| Field | Type | Required | Description |
|---|---|---|---|
period | string | yes | One of day, week, week-from-today, month, quarter, year. An invalid period is an error. |
units | integer | yes | Number of periods. |
anniversary | boolean | no | When true, only the month and day are compared — the year is ignored. Useful for birthdays and anniversaries. |
year, month, day | integer | no | Anchor the range to a specific date instead of today. All three must be supplied together; if any is missing, the range is anchored to today. |
text
| Operator | Value | Description |
|---|---|---|
contains | string | The field contains the value. |
contains_like | string | The field contains the value, with spaces treated as wildcards. |
does_not_contain | string | The field does not contain the value. |
does_not_contain_like | string | The field does not contain the value, with spaces treated as wildcards. |
starts_with | string | The field starts with the value. |
ends_with | string | The field ends with the value. |
word_boundary | string | The field contains the value at a word boundary — the start of the field, or following a space. |
like | string | The field is like the value, with % as a wildcard. |
not_like | string | The field is not like the value, with % as a wildcard. |
equal | string | Exactly equal. |
not_equal | string | Not exactly equal. |
in | [string] | Exactly equal to one of the values. |
not_in | [string] | Not equal to any of the values. |
is_set | none | The field has a value. |
is_not_set | none | The field has no value. |
date
A date string is yyyy-mm-dd.
| Operator | Value | Description |
|---|---|---|
equal | date string | Exactly equal. |
not_equal | date string | Not equal. |
less_than | date string | Earlier than the value. |
less_than_or_equal | date string | Earlier than or equal to the value. |
greater_than | date string | Later than the value. |
greater_than_or_equal | date string | Later than or equal to the value. |
between | range | Within [lbound, ubound], inclusive. |
not_between | range | Outside [lbound, ubound]. |
in | [date string] | Equal to one of the values. |
not_in | [date string] | Not equal to any of the values. |
in_the_last | relative range | Within the trailing window ending today. |
not_in_the_last | relative range | Outside the trailing window. |
in_the_next | relative range | Within the forward window starting today. |
not_in_the_next | relative range | Outside the forward window. |
in_the_previous | relative range | Within the previous complete calendar period(s). |
not_in_the_previous | relative range | Outside the previous complete calendar period(s). |
in_the_current | relative range | Within the current calendar period. |
not_in_the_current | relative range | Outside the current calendar period. |
in_the_upcoming | relative range | Within the upcoming calendar period(s). |
not_in_the_upcoming | relative range | Outside the upcoming calendar period(s). |
is_set | none | The field has a value. |
is_not_set | none | The field is empty. |
Rolling windows vs calendar periods
in_the_last/in_the_nextmeasure a rolling window from today —{"period": "month", "units": 1}means the last 30-ish days.in_the_previous/in_the_current/in_the_upcomingsnap to calendar boundaries — the same value means all of last month, 1st to last day. Pick deliberately: for period-over-period reporting you almost always want the calendar-period family.
date_parts
Like date, but the exact-comparison operators can compare individual components of a date. Every date operator is accepted; only the six exact comparisons behave differently:
| Operator | Value | Description |
|---|---|---|
equal, not_equal, less_than, less_than_or_equal, greater_than, greater_than_or_equal | parts object | Component comparison — see below. |
between / not_between | range | Within / outside [lbound, ubound], inclusive. |
in / not_in | [date string] | Equal to one of the values / to none of them. |
in_the_last, in_the_next (and not_*) | relative range | Rolling window from today. |
in_the_previous, in_the_current, in_the_upcoming (and not_*) | relative range | Calendar period(s). |
is_set / is_not_set | none | The field has a value / is empty. |
For everything except the six exact comparisons, the value shapes and behavior are identical to date above, including the rolling-vs-calendar distinction.
The parts object — each component is optional:
{ "year": 2026, "month": 8, "day": 14 }How the exact comparisons read it:
- All three parts — a true date comparison.
equalmatches the whole day. year+monthonly, withgreater_than[_or_equal]/less_than[_or_equal]— a true date comparison anchored to the month's last day (greater_*) or first day (less_*), so "after July 2026" and "before July 2026" mean what they say.- Any other partial combination — each supplied part is compared independently:
{ "month": 8, "day": 14 }withequalmatches an August 14 in any year (an anniversary match). With inequality operators this compares each component separately —month > 8 AND day > 14— which is rarely what a date question means.
Partial parts with inequality operators compare components, not datesOnly the two forms above collapse to a real date comparison. Anything else — e.g.
greater_thanwith onlymonthandday— is evaluated per component. For a real date cutoff, supply all three parts or usebetween.
number
| Operator | Value | Description |
|---|---|---|
equal | number | Exactly equal. |
not_equal | number | Not equal. |
less_than | number | Less than the value. |
less_than_or_equal | number | Less than or equal to the value. |
greater_than | number | Greater than the value. |
greater_than_or_equal | number | Greater than or equal to the value. |
between | range | Between the bounds, inclusive. |
not_between | range | Outside the bounds. |
in | [number] | Equal to one of the values. |
not_in | [number] | Not equal to any of the values. |
is_set | none | The field is not empty. |
is_not_set | none | The field is empty. |
currency
Identical to number, except values are in cents — the value you send is multiplied by 100 before comparison.
time
Identical to number, except it compares the time portion of a timestamp rather than a number.
boolean
| Operator | Value | Description |
|---|---|---|
is_set | none | The field is exactly '1'. |
is_not_set | none | The field is anything other than '1'. |
boolean_strict
Like boolean, but distinguishes explicit false from unset: '1' is true, '0' is false, anything else is unknown.
| Operator | Value | Description |
|---|---|---|
is_set | none | The field is exactly '1'. |
is_not_set | none | The field is exactly '0'. |
unknown | none | The field is neither '1' nor '0'. |
Running a saved search
Pass a saved search's id in configuration.search_id to run it. There is no separate endpoint for saved-search results.
{
"configuration": { "search_id": 1234 },
"filters": {}
}Whose filters actually run depends on the caller's relationship to the saved search:
| Caller | Result |
|---|---|
| Owner or editor of the saved search | Your submitted filters run — this supports previewing unsaved edits. |
| Shared into the saved search, but cannot edit it | The stored filters run. Your submitted filters are ignored. |
| Not shared into the saved search at all | search_id is ignored entirely and your submitted filters run. |
Asearch_idyou do not have access to fails open, not closedIf the authenticated user is not shared into that saved search, the API does not error. It silently discards
search_idand runs whatever you put infilters— so"filters": {}returns a broad, unfiltered result set that can easily be mistaken for a successful saved-search run.Before relying on
search_id, confirm the integration's user is shared into the saved search. A safer pattern for automation:GET /saved_searches/search/{id}to read the stored filter tree, then submit that tree asfiltersyourself.
GET /saved_searches/search/{search_id}/individualsreturns the people the saved search is shared with — its collaborators — not its results. It is easy to mistake for a results endpoint because it returns individual records.
Running a saved search updates its "last run" timestamp.
Examples
A simple comparison
All individuals with the first name "Tom":
{
"configuration": { "return_search_results": true },
"filters": {
"type": "constraint",
"id": "first_name",
"operator": "equal",
"value": "Tom",
"invert": false
}
}A grouped comparison
All individuals who are male and over 20:
{
"configuration": { "return_search_results": true },
"filters": {
"type": "group",
"operator": "and",
"invert": false,
"conditions": [
{
"type": "constraint",
"id": "gender",
"operator": "equal",
"value": "m",
"invert": false
},
{
"type": "constraint",
"id": "age",
"operator": "greater_than",
"value": 20,
"invert": false
}
]
}
}A nested comparison
All individuals who are male and over 20, or who are named Tom:
All individuals who are male and over 20, or who are named Tom:
{
"configuration": { "return_search_results": true },
"filters": {
"type": "group",
"operator": "or",
"invert": false,
"conditions": [
{
"type": "constraint",
"id": "first_name",
"operator": "equal",
"value": "Tom",
"invert": false
},
{
"type": "group",
"operator": "and",
"invert": false,
"conditions": [
{
"type": "constraint",
"id": "gender",
"operator": "equal",
"value": "m",
"invert": false
},
{
"type": "constraint",
"id": "age",
"operator": "greater_than",
"value": 20,
"invert": false
}
]
}
]
}
}Reporting over a full population, sorted and paged
Everyone created in the last quarter, including inactive and deceased profiles, oldest first:
{
"configuration": {
"return_search_results": true,
"filter_profile_type": ["active", "inactive", "deceased"],
"order_by": [{ "column": "date_created", "direction": "asc" }]
},
"filters": {
"type": "constraint",
"id": "date_created",
"operator": "in_the_previous",
"value": { "period": "quarter", "units": 1 },
"invert": false
}
}Request this as POST /search/individuals/results?per_page=100&page=1 and page until you get a short page.
