Skip to main content

Advanced user search

Directly using Management API to leverage advanced user search conditions.

Perform a search request​

Use GET /api/users for searching users. Note it is a Management API that requires auth like others. See Interact with Management API for the interaction recipe.

Sample​

Request

curl \
--location \
--request GET \
'http://<your-logto-endpoint>/api/users?search=%25alice%25'

Response

An array of User entity.

[
{
"id": "MgUzzDsyX0iB",
"username": "alice_123",
"primaryEmail": "alice@some.email.domain",
"primaryPhone": null,
"name": null,
"avatar": null
// ...
}
]

Parameters​

A search request consists of the following parameter keys:

  • Search keywords: search, search.*
  • Search mode for fields: mode, mode.* (default value 'like', available ['exact', 'like', 'similar_to', 'posix'])
  • Joint mode: joint or jointMode (default value 'or', available ['or', 'and'])
  • Is case-sensitive: isCaseSensitive (default value false)

This API has pagination enabled.

Let's go through them via some examples. All search params will be formatted as a constructor of URLSearchParams.

warning:

Search mode is set to like by default, which uses Approximate string matching ("fuzzy search").

note:

All fuzzy search modes only support matching one value per field. If you need to match multiple values for a single field, you should use the "exact" mode. See Exact match and case sensitivity for details.

If you want to perform a fuzzy search over all available fields, just provide a value for key search. It will use the like operator under the hood:

new URLSearchParams([['search', '%foo%']]);

This search will iterate over all available fields in a user search, i.e. id, primaryEmail, primaryPhone, username, name.

Specify fields​

What if you want to limit the search in name only? To search someone that includes foo in their name, just use the . symbol to specify the field:

new URLSearchParams([['search.name', '%foo%']]);

Remember nested fields are not supported, e.g. search.name.first will result an error.

You can also specify multiple fields at the same time:

new URLSearchParams([
['search.name', '%foo%'],
['search.primaryEmail', '%@gmail.com'],
]);

Means to search users that have foo in name OR their email ends with @gmail.com.

Changing the joint mode​

If you want the API only returns the result that satisfies ALL the conditions, set the joint mode to and:

new URLSearchParams([
['search.name', '%foo%'],
['search.primaryEmail', '%@gmail.com'],
['joint', 'and'],
]);

Means to search users that have foo in name AND their email ends with @gmail.com.

Exact match and case sensitivity​

Say you want to search whose name is exact "Alice". You can set mode.name to use exact match.

new URLSearchParams([
['search.name', 'Alice'],
['mode.name', 'exact'],
]);

You may find it has the same effect when using the like mode (default) v.s. specifying exact. One difference is exact mode uses = for comparing while like uses like or ilike. Theoretically = should have a better performance.

Plus, in exact mode, you can pass multiple values for matching, and they will be connected with or:

new URLSearchParams([
['search.name', 'Alice'],
['search.name', 'Bob'],
['mode.name', 'exact'],
]);

It will match the users with name "Alice" OR "Bob".

By default search is case-insensitive. To be more precise, set the search as case-sensitive:

new URLSearchParams([
['search.name', 'Alice'],
['search.name', 'Bob'],
['mode.name', 'exact'],
['isCaseSensitive', 'true'],
]);

Note isCaseSensitive is a global config. Thus EVERY field will follow it.

Regular expression (RegEx)​

PostgreSQL supports two types of regular expressions, similar to and posix. Set mode to similar_to or posix to search by regular expressions:

new URLSearchParams([
['search', '^T.?m Scot+$'],
['mode', 'posix'],
]);

Note Mode similar_to only works in case-sensitive searches.

Match mode override​

By default, all keywords will inherit the match mode from the general search:

new URLSearchParams([
['search', '^T.?m Scot+$'],
['mode', 'posix'],
['search.primaryEmail', 'tom%'], // Posix mode
['joint', 'and'],
]);

To override for specific field:

new URLSearchParams([
['search', '^T.?m Scot+$'],
['mode', 'posix'],
['search.primaryEmail', 'tom%'], // Like mode
['mode.primaryEmail', 'like'],
['search.phone', '0{3,}'], // Posix mode
['joint', 'and'],
]);

Look up by external identity​

To find the user linked to a social or enterprise SSO identity, pass the following three query parameters together for an exact lookup:

  • identityType: social for a social connector identity, or sso for an enterprise SSO identity.
  • identityProvider: the connector target for a social identity (for example, dingtalk), or the issuer for an enterprise SSO identity.
  • identityId: the user identifier issued by the external identity provider.
// Find the user linked to a DingTalk social identity
new URLSearchParams([
['identityType', 'social'],
['identityProvider', 'dingtalk'],
['identityId', 'dingtalk-open-id'],
]);
// Find the user linked to an enterprise SSO identity
new URLSearchParams([
['identityType', 'sso'],
['identityProvider', 'https://example.com/issuer'],
['identityId', 'enterprise-user-id'],
]);

The identity filter is combined with other search filters using AND logic. For example, to further narrow the identity lookup with a keyword search:

new URLSearchParams([
['identityType', 'social'],
['identityProvider', 'dingtalk'],
['identityId', 'dingtalk-open-id'],
['search', '%foo%'],
]);

Look up by custom data​

To find users by the exact values stored in their custom data, pass one or more customData query parameters to GET /api/users. Each value must be a URL-encoded JSON object:

curl \
--get \
--data-urlencode 'customData={"qq":114514}' \
'http://<your-logto-endpoint>/api/users'

--data-urlencode encodes the JSON object for you. The equivalent request URL is GET /api/users?customData=%7B%22qq%22%3A114514%7D.

note:

Custom data lookups can be slower when a tenant has a large number of users. Combine customData with another selective filter, such as search or an identity lookup, to narrow the query.

Logto matches the value against the user's custom_data using JSONB containment. Only the keys included in the value need to match; other keys in the user's custom data are ignored.

  • Types are strict: the number 114514 does not match the string "114514".
  • Strings are case-sensitive.
  • null matches only an explicit JSON null, not a missing key.
  • Nested objects are matched recursively.
  • Arrays are matched by element, regardless of order or duplicates.

All keys within one JSON object must match (AND). If you pass customData more than once, a user matches when any of the objects matches (OR):

curl \
--get \
--data-urlencode 'customData={"qq":114514}' \
--data-urlencode 'customData={"wechat":"logto"}' \
'http://<your-logto-endpoint>/api/users'

The custom data filter is always combined with other filters using AND, including search, identity lookup (identityType, identityProvider, and identityId), excludeRoleId, and excludeOrganizationId. For example, the following request returns users whose qq is 114514 and whose username contains alice:

curl \
--get \
--data-urlencode 'customData={"qq":114514}' \
--data-urlencode 'search.username=%alice%' \
'http://<your-logto-endpoint>/api/users'

The mode, joint, and isCaseSensitive parameters only affect keyword searches and have no effect on customData.

The API returns 400 Bad Request when a customData value is not a valid JSON object (for example, [] or 114514), is an empty object {}, or contains an empty object or array at any level (for example, {"profile":{}} or {"tags":[]}). Nested parameter names such as customData.qq, customData[qq], and mode.customData are also rejected with 400.