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.

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

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.