Advanced identity search via API
Advanced identity search allows you to perform full-text search on identity traits, public and admin metadata, combined with arbitrary filters and faceting.
- New to Ory? Talk to the team about features and plans.
- Already a customer? Open a support ticket.
Getting started
Ory exposes an advanced Identity Search API that is compatible with the Typesense search protocol. You can use existing Typesense SDKs to interact with it. Pass your Ory Network API key where you would normally pass the Typesense API key, and use the following base URL:
https://{your-ory-slug}.projects.oryapis.com/admin/preview/search/v0beta1
and query the collection named identities.
See the following examples for how to configure and use the Typesense SDKs to search for identities.
- cURL
- Go
- Java
export ORY_API_KEY="ory_pat_XXXXXXXXXXXXXXXX"
export ORY_SLUG="upbeat-lalande-zu8omm6wwp" # replace with your Ory slug
# List identities via Search API
curl -H "Authorization: Bearer $ORY_API_KEY" \
"https://$ORY_SLUG.projects.oryapis.com/admin/preview/search/v0beta1/collections/identities/documents/search?q=*"
# Search for "foo" in the email trait
curl -H "Authorization: Bearer $ORY_API_KEY" \
"https://$ORY_SLUG.projects.oryapis.com/admin/preview/search/v0beta1/collections/identities/documents/search?q=foo&query_by=traits.email"
package search
import (
"context"
"fmt"
"os"
ory "github.com/ory/client-go"
"github.com/typesense/typesense-go/v3/typesense"
typesenseapi "github.com/typesense/typesense-go/v3/typesense/api"
)
var (
ORY_PROJECT_URL = os.Getenv("ORY_PROJECT_URL")
ORY_API_KEY = os.Getenv("ORY_API_KEY")
)
func main() {
ctx := context.WithValue(context.Background(), ory.ContextAccessToken, ORY_API_KEY)
// Initialize Ory client
cfg := ory.NewConfiguration()
cfg.Servers = ory.ServerConfigurations{{URL: ORY_PROJECT_URL}}
oryClient := ory.NewAPIClient(cfg)
// Initialize search client
searchClient := typesense.NewClient(
typesense.WithAPIKey(ORY_API_KEY), // Use your Ory API key here
typesense.WithServer(ORY_PROJECT_URL+"/admin/preview/search/v0beta1/"), // configure the base URL to the Ory Search API endpoint
)
// List identities via Search API
list, err := searchClient.Collection("identities").Documents().Search(ctx, &typesenseapi.SearchCollectionParams{
Q: ptr("*"),
})
if err != nil {
// handle error
}
for _, res := range *list.Hits {
doc := *res.Document
fmt.Printf("List result: %+v\n", doc)
}
// Search identities via Search API
search, err := searchClient.Collection("identities").Documents().Search(ctx, &typesenseapi.SearchCollectionParams{
Q: ptr("foo"),
QueryBy: ptr("traits"),
})
if err != nil {
// handle error
}
if search.Hits == nil || len(*search.Hits) == 0 {
fmt.Println("No search hits")
}
fmt.Println()
for _, res := range *search.Hits {
doc := *res.Document
fmt.Printf("Search result: %+v\n", doc)
}
// retrieve identity details from Identity API
first := (*search.Hits)[0]
identity, _, err := oryClient.IdentityAPI.GetIdentity(ctx, (*first.Document)["id"].(string)).Execute()
if err != nil {
// handle error
}
fmt.Printf("Identity: %+v\n", identity)
}
func NewOryClient(ctx context.Context, url string) *ory.APIClient {
cfg := ory.NewConfiguration()
cfg.Servers = ory.ServerConfigurations{{URL: url}}
return ory.NewAPIClient(cfg)
}
func ptr[A any](v A) *A {
return &v
}
package sh.ory.examples.search;
import org.typesense.api.Client;
import org.typesense.api.Configuration;
import org.typesense.model.SearchParameters;
import org.typesense.model.SearchResult;
import org.typesense.resources.Node;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
try {
Client searchClient = initializeSearchClient();
// Search identities via Search API
SearchResult searchResult = searchClient.collections("identities").documents()
.search(new SearchParameters().q("test@example.com").queryBy("traits.email"));
System.out.println("Search results: " + searchResult);
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
}
private static Client initializeSearchClient() {
try {
List<Node> nodes = new ArrayList<>();
Node node = new Node("", "", "");
// Set the base URL to the Ory Search API endpoint
node.baseUrl = System.getenv("ORY_BASE_URL") + "/admin/preview/search/v0beta1";
nodes.add(node);
Configuration config = new Configuration(
nodes,
Duration.ofSeconds(5),
System.getenv("ORY_API_KEY")); // Use your Ory API key here
return new Client(config);
} catch (Exception e) {
System.err.println("Error initializing search client: " + e.getMessage());
e.printStackTrace();
return null;
}
}
}
Data model
The collection identities contains one document per identity. Each document contains the following fields:
{
"id": "d52d5bdb-74b4-4aa0-b706-d1e9c853bd81", // the identity ID
"region": "eu-central", // in which Ory Network region the identity's personal data is stored, facet
"organization_id": "org-id-123", // optional, facet
"external_id": "external-id-123", // optional, indexed
"created_at": 1725031437, // UNIX timestamp, facet, sortable
"updated_at": 1758115258, // UNIX timestamp, facet, sortable
"state": "active", // "inactive", "deleted", facet
"schema_id": "preset://email", // identity schema ID, facet
"available_aal": "aal1", // "aal2" etc, facet
"metadata_admin": {
"role": "user" // custom admin metadata, indexed, facet, search via `query_by=metadata_admin.role`
},
"metadata_public": {
"foo": "bar" // custom public metadata, indexed, facet, search via `query_by=metadata_public.foo`
},
"traits": {
"email": "wgiho@agpaa.com" // traits based on identity schema, indexed, search via `query_by=traits.email`
}
}
This API provides full-text search into identity data, not a database. The index is updated asynchronously and results are ranked for relevance and discovery. Do not use it to answer correctness-critical questions such as organization membership or permissions — use the identity management APIs and Ory Permissions for those.
Advanced search example
Please refer to the Typesense documentation for details on how to construct arbitrary search queries. An example:
- cURL
- sample result
export ORY_API_KEY="ory_pat_XXXXXXXXXXXXXXXX"
export ORY_SLUG="zealous-perlman-yvt3ifefzf" # replace with your Ory slug
# list identities which do not have two-factor authentication enabled,
# resolved by the organization to which they belong,
# ordered by creation date (newest first),
# and limited to 20 per page
curl -H "Authorization: Bearer $ORY_API_KEY" \
"https://$ORY_SLUG.projects.oryapis.com/admin/preview/search/v0beta1/collections/identities/documents/search" \
--url-query 'q=*' \
--url-query 'filter_by=available_aal:!=aal2' \
--url-query 'facet_by=organization_id' \
--url-query 'sort_by=created_at:desc' \
--url-query 'per_page=5'
{
"facet_counts": [
{
"counts": [
{
"count": 24,
"highlighted": "68efa2e9-fcd5-4104-8f49-7c51fa5752e2",
"value": "68efa2e9-fcd5-4104-8f49-7c51fa5752e2"
},
{
"count": 20,
"highlighted": "d936269a-2b49-4077-b4c8-2a256979fd27",
"value": "d936269a-2b49-4077-b4c8-2a256979fd27"
}
],
"field_name": "organization_id",
"stats": {
"avg": 0,
"sum": 0,
"total_values": 2
}
}
],
"found": 56,
"hits": [
{
"document": {
"available_aal": "aal0",
"created_at": 1781570773,
"id": "f329a237-3725-495f-ad5b-ceb28bb77609",
"nid": "8180c44b-2f36-4790-b22e-713cb3b60425",
"organization_id": "d936269a-2b49-4077-b4c8-2a256979fd27",
"region": "asia-northeast",
"schema_id": "preset://basic",
"state": "active",
"traits": {
"email": "kimberly.schmidt87@laposte.net",
"name": {
"first": "Kimberly",
"last": "Schmidt"
}
},
"updated_at": 1781710590
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal0",
"created_at": 1781133323,
"id": "5f549865-b8ac-48a1-a7e2-6cdddf4645e3",
"nid": "8180c44b-2f36-4790-b22e-713cb3b60425",
"organization_id": "68efa2e9-fcd5-4104-8f49-7c51fa5752e2",
"region": "asia-northeast",
"schema_id": "preset://basic",
"state": "active",
"traits": {
"email": "wolfgangcampbell137@ymail.com",
"name": {
"first": "Wolfgang",
"last": "Campbell"
}
},
"updated_at": 1781710601
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal0",
"created_at": 1776824080,
"id": "ec946791-8c24-4848-9916-7a98285cbd3e",
"nid": "8180c44b-2f36-4790-b22e-713cb3b60425",
"organization_id": "68efa2e9-fcd5-4104-8f49-7c51fa5752e2",
"region": "asia-northeast",
"schema_id": "preset://basic",
"state": "active",
"traits": {
"email": "alainlopez11@zoho.com",
"name": {
"first": "Alain",
"last": "Lopez"
}
},
"updated_at": 1781710574
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal0",
"created_at": 1773833986,
"id": "4f0ce13f-ffff-47b4-a853-b90fe438e6c7",
"nid": "8180c44b-2f36-4790-b22e-713cb3b60425",
"organization_id": "d936269a-2b49-4077-b4c8-2a256979fd27",
"region": "us-west",
"schema_id": "preset://basic",
"state": "active",
"traits": {
"email": "jacob.d36@msn.com",
"name": {
"first": "Jacob",
"last": "Durand"
}
},
"updated_at": 1781710579
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal0",
"created_at": 1773463258,
"id": "4f48a1bc-1f8f-4592-a36a-d9c799ae08da",
"nid": "8180c44b-2f36-4790-b22e-713cb3b60425",
"region": "us-west",
"schema_id": "preset://basic",
"state": "active",
"traits": {
"email": "christophe_patel8@outlook.de",
"name": {
"first": "Christophe",
"last": "Patel"
}
},
"updated_at": 1781710646
},
"highlight": {},
"highlights": []
}
],
"out_of": 56,
"page": 1,
"request_params": {
"collection_name": "identities",
"per_page": 5,
"q": "*"
},
"search_cutoff": false,
"search_time_ms": 2
}
To avoid timing out search requests, specify the exact fields you want to search in using the query_by parameter. For example,
to search for identities by email, use query_by=traits.email.
Most fields support infix-searching as well, so queries like q=@example.com&infix=always&query_by=traits.email are possible.
Be mindful that infix searches are considerably more expensive to run, so to avoid timing out search requests, use them only when necessary.
Search result set limit
The identity search API does not support pagination (unlike the List Identities API) and will return a maximum of 250 results per request. If you are frequently hitting this limit, consider refining your search queries to reduce the number of results returned. To paginate through all your identities, use the List Identities API.
Consistency and availability
The search index powering advanced identity search is eventually consistent with the main identity store. This means that there will be a delay between creating, updating, or deleting an identity and the changes being reflected in search results. We try to aim for a delay of less than one second during normal operation, but cannot provide any guarantees. The search index will have to be rebuilt periodically, during which time we cannot service search requests.
We recommend you use the search API for non-time-critical use cases, such as admin UIs or background jobs, where you need the full capability of full-text search, filtering, faceting, and sorting. Do not use it to answer correctness-critical questions such as organization membership or permissions — use the identity management APIs and Ory Permissions for those.
Before displaying search results to end-users or otherwise using the search results for business logic, we recommend you always fetch the full identity from the main identity store using the identity ID returned in search results to ensure you have the most up-to-date information.
