Search Configuration

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
}
FieldTypeRequiredDescription
type"constraint"yesNode discriminator.
idstringyesThe field to search. See the individual search params for the full list.
operatorstringyesMust be valid for the field's data type — see Data Types and Operators.
valuemixeddependsShape depends on the data type and operator. Omit for is_set / is_not_set.
invertbooleannoNegates the constraint. Defaults to false.
❗️

The key is invert, not inverted

A 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": []
}
FieldTypeRequiredDescription
type"group"yesNode discriminator.
operator"and" | "or"yesHow the children combine.
conditionsarray of nodesyesConstraint nodes, group nodes, or a mix.
invertbooleannoNegates 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

KeyTypeDefaultDescription
return_search_resultsbooleantrueWhether 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_positionsarray[]Also return family members of matched individuals, by position. Any of PRIMARY_CONTACT, SPOUSE, CHILD, OTHER.
order_byarray[]Sort columns in precedence order. See below.
columnsarray of string[]Columns to include in the output. Only used by the export action — ignored by /results.
search_idinteger—Run a saved search. See Running a saved search.

order_by

Each entry is an object:

{
  "order_by": [
    { "column": "last_name", "direction": "asc" },
    { "column": "first_name", "direction": "asc" }
  ]
}
FieldTypeRequiredDescription
columnstringyesA sortable column for the domain. Unknown columns are rejected.
direction"asc" | "desc"noCase-insensitive. Defaults to asc.
📘

Always set order_by when paging

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

KeyTypeDefaultDescription
filter_profile_typearray["active"]Which profile types to include. Any of active, inactive, deceased.
include_inactivebooleanfalseLegacy alternative to adding inactive to filter_profile_type. Either mechanism works; they are OR'd together.
include_deceasedbooleanfalseLegacy alternative to adding deceased to filter_profile_type.
include_laubooleantrueInclude limited access users. Set false to exclude them.
include_pendingbooleantrueInclude pending (unapproved) profiles. Set false to exclude them.
include_unlistedbooleanpermission-derivedInclude unlisted individuals. See the note below.
exclude_childrenbooleanfalseExclude individuals below the organization's child-work age.
❗️

The default population is active profiles only

If 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_pending and include_lau both default to true. A plain individuals search already returns pending profiles mixed in with approved ones. To report on approval state, filter on the approval_status field 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_parts or date_deceased_month constraint, deceased profiles are included regardless of filter_profile_type.
  • An is_inactive constraint forces inactive profiles in. Same behavior.

include_unlisted is permission-governed

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

KeyWhy
unlisted_campus_idsThe campuses where the caller may view unlisted individuals. Always overwritten from the caller's permissions.
child_work_approved_campus_idsDerived from the caller's child-work approvals.
is_saved_search_collaboratorSet internally while resolving search_id.
client_localeDerived from the request.
return_idsSet internally by search actions.
check_permissionsDerived 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.

ParameterTypeDefaultDescription
pageinteger11-based page number.
per_pageinteger25Page size. One of 25, 50, 75, 100.
exclude_totalbooleanfalseSkip 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_total is a query parameter, not a configuration key. Placing it in configuration has 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 }
FieldTypeRequiredDescription
periodstringyesOne of day, week, week-from-today, month, quarter, year. An invalid period is an error.
unitsintegeryesNumber of periods.
anniversarybooleannoWhen true, only the month and day are compared — the year is ignored. Useful for birthdays and anniversaries.
year, month, dayintegernoAnchor 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

OperatorValueDescription
containsstringThe field contains the value.
contains_likestringThe field contains the value, with spaces treated as wildcards.
does_not_containstringThe field does not contain the value.
does_not_contain_likestringThe field does not contain the value, with spaces treated as wildcards.
starts_withstringThe field starts with the value.
ends_withstringThe field ends with the value.
word_boundarystringThe field contains the value at a word boundary — the start of the field, or following a space.
likestringThe field is like the value, with % as a wildcard.
not_likestringThe field is not like the value, with % as a wildcard.
equalstringExactly equal.
not_equalstringNot exactly equal.
in[string]Exactly equal to one of the values.
not_in[string]Not equal to any of the values.
is_setnoneThe field has a value.
is_not_setnoneThe field has no value.

date

A date string is yyyy-mm-dd.

OperatorValueDescription
equaldate stringExactly equal.
not_equaldate stringNot equal.
less_thandate stringEarlier than the value.
less_than_or_equaldate stringEarlier than or equal to the value.
greater_thandate stringLater than the value.
greater_than_or_equaldate stringLater than or equal to the value.
betweenrangeWithin [lbound, ubound], inclusive.
not_betweenrangeOutside [lbound, ubound].
in[date string]Equal to one of the values.
not_in[date string]Not equal to any of the values.
in_the_lastrelative rangeWithin the trailing window ending today.
not_in_the_lastrelative rangeOutside the trailing window.
in_the_nextrelative rangeWithin the forward window starting today.
not_in_the_nextrelative rangeOutside the forward window.
in_the_previousrelative rangeWithin the previous complete calendar period(s).
not_in_the_previousrelative rangeOutside the previous complete calendar period(s).
in_the_currentrelative rangeWithin the current calendar period.
not_in_the_currentrelative rangeOutside the current calendar period.
in_the_upcomingrelative rangeWithin the upcoming calendar period(s).
not_in_the_upcomingrelative rangeOutside the upcoming calendar period(s).
is_setnoneThe field has a value.
is_not_setnoneThe field is empty.
📘

Rolling windows vs calendar periods

in_the_last / in_the_next measure a rolling window from today — {"period": "month", "units": 1} means the last 30-ish days. in_the_previous / in_the_current / in_the_upcoming snap 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:

OperatorValueDescription
equal, not_equal, less_than, less_than_or_equal, greater_than, greater_than_or_equalparts objectComponent comparison — see below.
between / not_betweenrangeWithin / 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 rangeRolling window from today.
in_the_previous, in_the_current, in_the_upcoming (and not_*)relative rangeCalendar period(s).
is_set / is_not_setnoneThe 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. equal matches the whole day.
  • year + month only, with greater_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 } with equal matches 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 dates

Only the two forms above collapse to a real date comparison. Anything else — e.g. greater_than with only month and day — is evaluated per component. For a real date cutoff, supply all three parts or use between.

number

OperatorValueDescription
equalnumberExactly equal.
not_equalnumberNot equal.
less_thannumberLess than the value.
less_than_or_equalnumberLess than or equal to the value.
greater_thannumberGreater than the value.
greater_than_or_equalnumberGreater than or equal to the value.
betweenrangeBetween the bounds, inclusive.
not_betweenrangeOutside the bounds.
in[number]Equal to one of the values.
not_in[number]Not equal to any of the values.
is_setnoneThe field is not empty.
is_not_setnoneThe 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

OperatorValueDescription
is_setnoneThe field is exactly '1'.
is_not_setnoneThe 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.

OperatorValueDescription
is_setnoneThe field is exactly '1'.
is_not_setnoneThe field is exactly '0'.
unknownnoneThe 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:

CallerResult
Owner or editor of the saved searchYour submitted filters run — this supports previewing unsaved edits.
Shared into the saved search, but cannot edit itThe stored filters run. Your submitted filters are ignored.
Not shared into the saved search at allsearch_id is ignored entirely and your submitted filters run.
❗️

A search_id you do not have access to fails open, not closed

If the authenticated user is not shared into that saved search, the API does not error. It silently discards search_id and runs whatever you put in filters — 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 as filters yourself.

📘

GET /saved_searches/search/{search_id}/individuals returns 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.