Person API Filter Query Paramater

Filtering data from the Person API.

Filter results of query using the filter query parameter family .

Case Sensitivity

The attribute name is case-sensitive (example: lastName ) but the value is not (example: smith and SMITH will match Smith ).

Filtering On Multiple Values

Multiple values can be included in a filter by using a comma as a delimiter, which will execute an \"OR\" query for the included values (e.g. /people?filter[firstName]=Jane,Jon will find people that have the firstName of Jane or Jon).

There is a limit of 10 values per filter.

Multiple filters for the same attribute cannot be sent in a single request (e.g. /people?filter[firstName]=Jane&filter[firstName]=Jon ). Instead, use a single filter with comma-separated values.

Escaping A Comma

Use a backslash ( \ ) to escape a comma, if the filter value contains a comma that shouldn't be used as a delimiter. For example, /people?filter[officeAddress]=123 Main St\, Madison will find people with the office address \"123 Main St, Madison\", but /people?filter[officeAddress]=123 Main St, Madison would find people who have the office address \"123 Main St\" or \" Madison\".

Partial String Searches

This query parameter can also be used to facilitate partial string searches. For this feature, prefix the value with one of the supported keywords followed by a : . For example filter[lastName]=STARTS_WITH:smi , will return results for people with lastname that starts with smi .

You can also combine multiple operations on separate values. For example: filter[firstName]=STARTS_WITH:ste,STARTS_WITH:da,ENDS_WITH:ven,robert , will return results for people with firstname that starts with ste , starts with da , ends with ven , or matches robert .

Supported operations are [ STARTS_WITH , CONTAINS , ENDS_WITH , IN ]. IN is only used for searching on values in a list attribute (see below).

IN Operator For List Attributes

The IN operator can be used to filter results based on specific values within list attributes. For example, the query /people?filter[jobs.relatedSupervisoryOrganizationIds]=IN:SO00001234,SO00004321 will return people who have SO00001234 , SO00004321 , or both in the relatedSupervisoryOrganizationIds attribute of any jobs resource.

Example Scenario

A user wants to collect emailAddress for all employees in DoIT and the Manager of DoIT. Let's assume, the Supervisory Organization ID for the manager of DoIT is SO00001234 and the Supervisory Organization ID for DoIT which they manage is SO00004321 :

  • Person A (Supervisor of the Manager of DoIT) has relatedSupervisoryOrganizationIds as SO00000000,SO00001234
  • Person B (Manager of DoIT) has relatedSupervisoryOrganizationIds as SO00000000,SO00001234,SO00004321
  • Person C (Employee in DoIT who manages a supervisory organization) has relatedSupervisoryOrganizationIds as SO00000000,SO00001234,SO00004321,SO00005432
  • Person D (Employee in a child supervisory organization of DoIT, ex. AIS) has relatedSupervisoryOrganizationIds as SO00000000,SO00001234,SO00004321,SO00005432

The query /people?filter[jobs.relatedSupervisoryOrganizationIds]=IN:SO00004321&fields[people]=emailAddress will return the emailAddress attribute for Person B, C, and D.

Person A will not be returned because they do not have SO00004321 in their relatedSupervisoryOrganizationIds .

Benefits Of Using The IN Operator

  • The IN operator simplifies querying by allowing multiple values to be checked in a single request.
  • It supports complex queries involving list attributes, making it easier to retrieve relevant data.

Range Operator For Date and Number Attributes

This query parameter can also be used to facilitate range searches on attributes of type Date and Number . For this feature, prefix the value with one of the supported keywords followed by a : . For example, filter[continuousServiceDate]=GREATER_THAN:2020-01-01 will return results for people with continuousServiceDate attribute greater than 2020-01-01 .

Unlike partial string searches, you cannot combine multiple range operators. For example, filter[continuousServiceDate]=GREATER_THAN:2020-01-01,LESS_THAN:2025-01-01 is not permitted.

Supported operations are [ GREATER_THAN , GREATER_THAN_OR_EQUAL , LESS_THAN , LESS_THAN_OR_EQUAL ].

NOT_IN Operator

The NOT_IN operator excludes people resources that contain only values from a given exclusion set. A people resource is included if at least one attribute or relationship attribute has a value outside the excluded set; it is excluded only when all related values are in the excluded set.

For example, the query /people?filter[jobs.employeeCategoryCode]=NOT_IN:SA,SH will exclude all people resources who have the value SA or SH in the employeeCategoryCode attribute in all of their jobs resources. The query will return people resources who have a value other than SA or SH in the employeeCategoryCode attribute in at least one of their related jobs resources.

If the people resource has no relationships or relationship attribute for the given filter attribute, it is excluded. So for the example above, a people resource with no jobs relationship will be excluded.

Quotes Are Significant Characters

Quotes are significant characters and should only be used if they're intended to be used in the filter (e.g. if someone is searching for "O'Brian")

Returns no values: filter[firstName]='STARTS_WITH:smi' Works: filter[firstName]=STARTS_WITH:smi

Filtering on Relationships

You can filter on the values of a person's relationships, such as data about their jobs or email addresses using the convention filter[<relationship>.<attribute>]=<value> . For example, filter[jobs.current]=true would return people who have at least one current job. Multiple filters on the same relationship will return results that match all of the filters. For example, the request /people?filter[jobs.current]=true&filter[jobs.payBasis]=Annual would return people who have at least one job that is current and has a payBasis of 'Annual'.

Filtering can also be done on sub-relationships (double, triple, etc. nested filters), for example, /people?filter[jobs.organizationStructure.departmentId]=CCH001 . In that example, the API would return people who have at least one job with an associated organizationStructure that has a departmentId of CCH001 .

Deeply-nested filters interact with other relationship filters in a similar way, as they are grouped together with their "parent" resource (e.g. for jobs.organizationStructure , the parent would be jobs ). For example, /people?filter[jobs.current]=true&filter[jobs.organizationStructure.departmentId]=CCH001 would return people who have at least one job that is current and has an organizationStructure.departmentId of CCH001 , and /people?filter[jobs.current]=true&filter[jobs.costAllocations.current]=false&filter[jobs.costAllocations.worktags.worktagType]=Grant would return people who have at least one job that is current and has a cost allocation that is both not current and contains a Grant -type worktag.

Filtering Included Resources

When you filter on relationships, as long as there is at least one match on a person's resources, all of the relationships you specify in the include parameter will be returned, even ones which do not match. For example, /people?filter[addresses.city]=Madison&include=addresses would return every address of every person who has at least one address in Madison, even any non-Madison addresses they may have. If you need to ensure the only sub-resources returned are ones relevant to your query, you can use the filterIncluded parameter, either on its own or in conjunction with the filter parameter, to completely remove any sub-resources you do not wish to see.

The format of filterIncluded is filterIncluded[<relationshipPath>:<attributePath>]=<values> , where <relationshipPath> is the resource you want to filter (the "what") and <attributePath> is the route from <relationshipPath> to the attribute to filter on (the "where"), which will be filtered on by <values> (the "how").

Here are some examples:

  • filterIncluded[addresses:city]=Madison&include=addresses
    • Only keep addresses whose city attribute is Madison .
    • Note that, like filter parameters, the values are treated case-insensitively.
  • filter[identifiers.current]=true&filter[identifiers.name]=pvi
    &filter[identifiers.value]=UW123A123&filterIncluded[identifiers:current]=true
    &filterIncluded[identifiers:name]=pvi&filterIncluded[identifiers:value]=UW123A123
    &include=identifiers
    • Locate everyone with at least one identifier that is current, whose name is pvi , and whose value is UW123A123 .
    • Then, only keep those identifiers which are current, have a name of pvi , and a value of UW123A123 .
  • filterIncluded[jobs.organizationStructure:costCenterId]=CC001111,CC002222,CC003333,CC004444
    &include=jobs,jobs.organizationStructure
    • For each job, only keep organizationStructures with a costCenterId matching CC001111 , CC002222 , CC003333 , or CC004444 .
    • Note that, if a job has an organizationStructure with a costCenterId of CC005555 , the job will still be present, but its organizationStructure will be removed.
  • filterIncluded[jobs:costAllocations.startDate]=GREATER_THAN_OR_EQUAL:2020-01-01
    &include=jobs,jobs.costAllocations
    • Only keep jobs that have at least 1 costAllocation that started on or after 2020-01-01 .
      • Any jobs with 0 costAllocations, a costAllocation with a null startDate , or a startDate before 2020-01-01 will be removed, along with all of those jobs' other related resources
        ( jobs.organizationStructure , jobs.costAllocations.worktags , etc.).
    • A job with two costAllocations, one with a startDate of 2019-12-01 and the other 2020-12-01 , will be present along with both of its costAllocations because only one of those costAllocations was required to match.
  • filterIncluded[jobs.costAllocations:current]=true
    &include=jobs,jobs.costAllocations,jobs.costAllocations.worktags
    • Keep all jobs, but only keep the costAllocations (and their associated worktags) that are current.
  • filterIncluded[jobs.costAllocations.worktags:worktagType]=Gift
    &include=jobs,jobs.costAllocations,jobs.costAllocations.worktags
    • Keep all jobs and costAllocations, but only keep the worktags that have a worktagType of Gift .
  • filterIncluded[jobs:managedSupervisoryOrganizationIds]=IN:SO00001234,SO00004321
    &include=jobs,jobs.costAllocations
    • Only keep jobs (and their associated costAllocations) that have managedSupervisoryOrganizationIds containing SO00001234 , SO00004321 , or both.
  • filterIncluded[jobs:current]=true
    &filterIncluded[jobs:businessTitle]=STARTS_WITH:Director,CONTAINS:Manager
    &filterIncluded[jobs.costAllocations:current]=true&include=jobs,jobs.costAllocations
    • Only keep jobs which are both current and have a businessTitle that starts with Director and/or contains Manager .
    • Of the remaining jobs' associated costAllocations, only keep the costAllocations which are current.
  • filterIncluded[jobs:current]=true
    &filterIncluded[jobs:organizationStructures.costCenterName]=CONTAINS:UWMSN
    &filterIncluded[jobs.costAllocations:worktags.worktagType]=Program
    &include=jobs,jobs.costAllocations,jobs.costAllocations.worktags,jobs.organizationStructures
    • Only keep jobs (and their associated organizationStructures) which are both current and have an organizationStructure whose costCenterName contains UWMSN .
    • Of the remaining jobs' associated costAllocations, only keep the costAllocations (and associated worktags) which have at least 1 worktag whose worktagType is Program .
  • /people/{id}/jobs?filterIncluded[costAllocations:worktags.name]=ENDS_WITH:Service Operations General
    &include=costAllocations,costAllocations.worktags
    • For each of this person's jobs, only keep the costAllocations that have at least 1 worktag whose name ends with Service Operations General .

Inclusion filtering is done after we fetch data (according to the filter parameters you provide) and only applies to the data which can be placed in the included section of the response; it does not modify the scope of your query.

You do not need to have the same values for filter as you do for filterIncluded , nor do you need to use them in conjunction, but you do need to ensure you are using these parameters correctly as they serve different purposes: filter reduces the population, the body of resources that your query defines (who you want to view); filterIncluded just refines the final output (what you want to view).

That is, if you go to /people?include=addresses&filterIncluded[addresses:city]=Madison , everyone will be returned, regardless of whether they have any addresses or what cities they are in, but those addresses which do end up in the included section will only be those whose city is Madison . If you want to ensure that only people with at least one Madison address are returned in addition to only seeing those Madison addresses, you must also use the filter parameter; in this case, you would go to /people?include=addresses&filter[addresses.city]=Madison&filterIncluded[addresses:city]=Madison .

You can use the filterIncluded query parameter:

  • On any route that supports inclusions and any relationship it supports including,
  • For any attribute that supports filters,
  • Using all the same operators that each attribute supports (for example, IN on jobs.relatedSupervisoryOrganizationIds and range operators such as GREATER_THAN on jobs.annualizedSalary ),
  • In conjunction with any other supported query parameter ( filter , fields , etc.), and
  • With the same limitations as filters (10 values per filter when matching multiple values, only one range operator per attribute, etc.).

However, you cannot use them on the base attributes of a resource because the standard filter query parameter already handles this case and filterIncluded only operates on sub-resources, the data which can end up in the included section of a response. For example:

  • On the /people endpoint, the base resource is people , so /people?filterIncluded[people:firstName]=Alice is not allowed as /people?filter[firstName]=Alice already accomplishes this.
  • On the /people/{id}/addresses endpoint, the base resource is addresses , so /people/{id}/addresses?filterIncluded[addresses:city]=Madison does not work because /people/{id}/addresses?filter[city]=Madison handles this filtering.

It should also be noted that filterIncluded filters out non-matching data in its entirety. That is:

  • /people?include=jobs&filter[jobs.current]=true&filterIncluded[jobs:current]=true
    • Any non-current jobs will be absent from the included section, but if you look at the jobs listed on a person (e.g. at data.people[0].relationships.jobs ), you will see the non-matching jobs were also filtered out there and the IDs present are only for current jobs.
  • /people/{id}/jobs?include=costAllocations&filterIncluded[costAllocations:current]=true
    • Any non-current costAllocations will be absent from both the included section and from the job (e.g. at data.jobs[0].relationships.costAllocations ).
  • /people/{id}/jobs?include=costAllocations,costAllocations.worktags
    &filterIncluded[costAllocations.worktags:worktagType]=Grant
    • Any non-grant worktags will be missing from both the included section of the response and from the relationships section of the costAllocation they are associated with.

Example Workflows

The Filter query parameter can be used for the identifier crosswalk workflow where you want to get additional identifiers for a person given one of their identifiers. For example: filter[identifiers.name]=netId&filter[identifiers.value]=exampleNetId&include=identifiers can be used to find a person's identifiers given their NetID exampleNetId , and filter[identifiers.name]=netId&filter[identifiers.value]=exampleNetId&filterIncluded[identifiers:name]=NOT_IN:netId&include=identifiers can be used to find all non-NetID identifiers given their NetID exampleNetId .

You can also use a similar query to get all the identifiers for a person using an old identifier such as filter[identifiers.name]=pvi&filter[identifiers.value]=oldPVI&include=identifiers , or filter[identifiers.name]=pvi&filter[identifiers.value]=oldPVI&filterIncluded[identifiers:current]=true&include=identifiers to only get the person's current identifiers.

Advanced Filters

The advanced filter query parameter, advancedFilter, offers greater control of your filters. It allows you to control both the logical and relationship grouping of your search. Advanced filters cannot be used in conjunction with the simple filter query parameter, filter .

The following functions are currently supported:

Advanced Filter Functions
Operation Function Example
Equals equals advancedFilter=equals(firstName, "John")
Starts with text startsWith advancedFilter=startsWith(lastName, "smi")
Contains text contains advancedFilter=contains(lastName, "mit")
Ends with text endsWith advancedFilter=endsWith(lastName, "ith")
Value in list attribute memberOf advancedFilter=jobs(memberOf(relatedSupervisoryOrganizationIds, "S123"))
Date/number greater than value greaterThan advancedFilter=greaterThan(dateOfBirth, "2000-01-01")
Date/number greater than or equal to value greaterThanOrEqual advancedFilter=jobs(greaterThanOrEqual(annualizedSalary, 50000))
Date/number less than value lessThan advancedFilter=lessThan(dateOfBirth, "2000-01-01")
Date/number less than or equal to value lessThanOrEqual advancedFilter=jobs(lessThanOrEqual(annualizedSalary, 50000))
Logical NOT grouping not advancedFilter=not(equals(firstName, "John"))
Logical AND grouping and advancedFilter=and(equals(firstName, "John"), equals(lastName, "Smith"))
Logical OR grouping or advancedFilter=or(equals(emailAddress, "jsmith@wisc.edu"), equals(officePhoneNumber, "123-456-7890"))
Relationship grouping "relationshipName" advancedFilter=jobs(equals(current, true))

Filter Values

Values in advanced filters can be one of the following:

  • Escaped string: A string surrounded by double quotes ( " ) that supports escaped characters with a backslash ( \ ). For example - advancedFilter=equals(firstName, "John") or advancedFilter=startsWith(lastName, "Doe\"doe")
  • Number: A number with or without decimal values. Signed numbers and exponential notation are not supported. For example - advancedFilter=jobs(equals(annualizedSalary, 123456.00))
  • Boolean: The values true or false . For example - advancedFilter=jobs(equals(current, true))
  • List of values: A list of escaped strings, numbers, or booleans surrounded by square brakets ( [] ) and delimated by commas ( , ). A list may contain up to 10 items. These values are evaluated with OR logic. The one exception is the date/number functions (like greaterThan ), which only accept a single value. For example - advancedFilter=equals(firstName, ["John", "James", "Janet"])

Logical Grouping

Logical grouping refers to functions like not , and or or that allow you to group multiple functions together. You may nest up to 5 level deep with logical grouping.

The groups and and or accept one or more functions as parameters. The group not accepts a single function as a parameter. Multiple functions can be passed to a not function by nesting an and or or function inside the not function.

Relationship Grouping

A relationship group is the function "relationshipName", i.e. advancedFilter=jobs(equals(current, true)) . A relationship group accepts one single function. Inside a relationship group, all attributes and additional relationships refer to attributes and additional relationships relative to the grouped relationship. Additionally, all functions in a single relationship group match against a single sub-resource in that relationship.

A relationship group will return False if there are no sub-resources in that relationship.

  • Simple relationship groups: To filter on a single attribute in a relationship, use a relationship group around the function for your attribute. For example, to find people with current jobs use the filter advancedFilter=jobs(equals(current, true)) .
  • Nested relationship groups: To access deeply nested sub-resources, use multiple relationship filters. For example, to find people with jobs, with a cost allocation, with a worktag, with a name that starts with "GR000036316" use the following filter:

advancedFilter=jobs(
    costAllocations(
        worktags(
            startsWith(name, "GR000036316")
        )
    )
)

  • Multiple relationship groups: To compare multiple filters against a relationship, you can use multiple copies of the same relationship group. For example, to find people with either a NetID of "jsmith" or a PVI of "UW12345" use the following filter:

advancedFilter=or(
    identifiers(
        and(
            equals(name, "netID"),
            equals(value, "jsmith")
        )
    ),
    identifiers(
        and(
            equals(name, "pvi"),
            equals(value, "UW12345")
        )
    )
)

Combining Relationships and NOT grouping

When combing relationships and not groups, the not group can be either inside or outside the relationship, producing different results. The following examples illustrate the difference:

  • People who don't have a job with the title "software engineer": The following query will return people who don't have a job with the title "software engineer". People with no jobs will be included. If a person has multiple jobs, only one job needs to have the title "software engineer" for the person to be excluded.

advancedFilter=not(
    jobs(
        equals(title, "software engineer"),
    ),
)

  • People who have a job that doesn't have the title "software engineer": The following query will return people who have a job that doesn't have the title "software engineer". People with no jobs will be excluded. If a person has multiple jobs, only one job needs to not have the title "software engineer" for the person to be included.

advancedFilter=jobs(
    not(
        equals(title, "software engineer"),
    ),
)

Comparison to Simple Filters

Most simple filter searches can be re-written to use advanced filters. Here are several examples of searches with the equivalent simple filters and advanced filters:

People with one of several identifiers * Simple Filter: filter[identifiers.value]=ABC123,DEF456,GHI789 * Advanced Filter: advancedFilter=identifiers(equals(value, ["ABC123", "DEF456", "GHI789"]))

People with a current job in a specific supervisory organization * Simple Filter: filter[jobs.supervisoryOrganizationId]=S0123&filter[jobs.current]=true * Advanced Filter: advancedFilter=jobs(and(equals(supervisoryOrganizationId, "S0123"), equals(current, true)))

People with a current job, with a current cost allocation, with a grant worktag * Simple Filter: filter[jobs.current]=true&filter[jobs.costAllocations.current]=true&filter[jobs.costAllocations.worktags.worktagType]=Grant * Advanced Filter:

advancedFilter=jobs(
    and(
        equals(current, true),
        costAllocations(
            and(
                equals(current, true),
                worktags(
                    equals(worktagType, "Grant")
                )
            )
        )
    )
)

Syntax Errors

If there is a syntax error in the advancedFilter parameter, a 400 Bad Request response will be returned with details about the error.

For example, if you send the request advancedFilter=equals(firstName, "john")) with an extra closing parenthesis, the response body will contain the following error message:

Syntax error in advanced filter query:
Unexpected token Token('RPAR', ')') at line 1, column 26.
Expected one of:
    *

This indicates that there was an unexpected right parenthesis at column 26 of the query. The list of expected tokens has an empty entry, indicating that the parser was expecting the end of the query.

Alternatively, if you send the request advancedFilter=equals(firstName "john") with a missing comma, the response body will contain the following error message:

Syntax error in advanced filter query:
Unexpected token Token('ESCAPED_STRING', '\"john\"') at line 1, column 18.
Expected one of:
    * COMMA
Previous tokens: [Token('WORD', 'firstName')].

This indicates that there was an unexpected escaped string at column 18 of the query. The list of expected tokens indicates that a comma was expected at that point. The previous tokens section shows that the last successfully parsed token was the word firstName .



Keywords:
person-api 
Doc ID:
158404
Owned by:
Jared K. in DoIT Enterprise Integration - API Team
Created:
2026-02-09
Updated:
2026-02-10
Sites:
DoIT Enterprise Integration - API Team