Searching Business Objects and Nodes
The RealityConnect API exposes two search routes with different scopes: business object search looks inside a twin for POIs, zones, assets, and their metadata, while node search looks across your organization’s node hierarchy (divisions, sites, folders, twins, asset libraries, and data files). Both routes are experimental and may change.
Endpoints
Section titled “Endpoints”| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /v1/twin/{contextId}/search | search:objects | Search business objects and metadata inside a twin |
GET | /v1/nodes/search | ReadHierarchy | Search the organization’s node hierarchy |
Searching business objects
Section titled “Searching business objects”GET /v1/twin/{contextId}/search searches POIs, zones, box assets, measures, and other business objects inside a single twin (or one of its sites/drafts, identified by contextId), matching on name and other filterable fields, asset metadata, and object-specific properties.
Query parameters
Section titled “Query parameters”| Parameter | Type | Notes |
|---|---|---|
query | string | Free-text, fuzzy-matched against name and metadata. Narrows results only when used alone. Combined with any other filter, it affects ranking and highlighting but does not exclude non-matches. See Combining filters. |
fieldFilters | array of JSON objects | Reserved-key filters. See below. |
metadataFilters | array of JSON objects | Match twin asset-metadata properties. See below. |
additionalPropertyFilters | array of JSON objects | Match object-type-specific properties (e.g. a POI’s icon). See below. |
page | integer | Positive, default 1. |
limit | integer | 1–50, default 50. |
searchNameQuery | string | Dedicated name filter, independent of query/fieldFilters name. |
nameMatch | exact | contains | Match mode for searchNameQuery. Default contains. |
createdById | uuid | Filter by creator. |
createdBefore / createdAfter | timestamp | Strictly before/after. Format YYYY-MM-DDTHH:mm:ss (no timezone offset, no milliseconds). |
updatedBefore / updatedAfter | timestamp | Same format. |
sortBy | name | createdAt | updatedAt | Sort field. |
sortDir | ASC | DESC | Sort direction. |
Filters
Section titled “Filters”fieldFilters, metadataFilters, and additionalPropertyFilters are each sent as one JSON object string per filter, repeated as a query parameter for multiple filters:
?fieldFilters={"key":"type","values":["poi"]}&fieldFilters={"key":"type","values":["zone"]}Each filter object has the shape { "key": string, "values": string[] }. Multiple values in one filter’s values array are OR’d.
fieldFilters only accepts these reserved keys. Any other key is silently ignored:
| Key | Matches |
|---|---|
type | Business object type. Accepts either the enum value (Poi, Zone, BoxAsset, CubePrimitive, PlanePrimitive, DistanceMeasure, CoordinateMeasure, DiameterMeasure, SurfaceMeasure, OrthogonalMeasure, Cut, ModelAsset) or its normalized kebab-case token: a hyphenated, lowercased version of the PascalCase name, e.g. BoxAsset → box-asset. |
asset-type | The object’s asset type (maps to assetTypeId). Exact match. |
name | Per-word match, not a phrase-anchored prefix. The value’s words are OR’d, so a match on any single word is enough. A non-last word must match a whole word in the name exactly to count; the last word matches as a prefix instead. {"key":"name","values":["Bulk POI 01"]} matches any name containing “Bulk”, or “POI”, or a word starting with “01”, not only names starting with that exact phrase. A single distinctive word gives the most precise filter. |
id | Exact object id match. |
fieldFilters values are always plain strings or enum tokens; the numeric/range syntax below does not apply to them.
metadataFilters and additionalPropertyFilters address a schema-defined metadata property (metadataFilters) or an object-type-specific property not part of common metadata (additionalPropertyFilters, e.g. a POI’s icon) by key. The key is <section>.<property>, using the exact casing and spacing defined in the twin’s metadata schema. There is no normalization, so a section named General with a property Grid column is addressed as General.Grid column, not general.gridcolumn. The inspection.pressure-style keys used in examples on this page are illustrative; your twin’s real keys depend entirely on how its metadata schema was authored.
Each metadata or object property is indexed as a string, a number, or a boolean, based on its value’s type, and the values syntax you must use depends on which one it is:
| Value syntax | Meaning |
|---|---|
| Plain string | Exact match, string-typed properties only. A numeric-typed property will not match a plain string; use a numeric comparison instead (see Numeric and range filters below). Multiple plain strings in values are OR’d. |
* or empty string | Skipped. If it’s the only value, the filter matches on key presence alone. |
Comparison or range syntax (=123, range:[1..10], etc.) | Numeric-typed properties only. See Numeric and range filters below. |
true / false | additionalPropertyFilters only. Matches a boolean property literally. metadataFilters has no syntax that matches a boolean-typed metadata property. |
Combining filters
Section titled “Combining filters”fieldFilters, metadataFilters, and additionalPropertyFilters combine with AND semantics: a result must satisfy every filter. Within a single filter, the values in its values array are OR’d, so two fieldFilters objects that share the same key behave as “either value matches,” while filters on different keys (or different filter arrays) all narrow the result set further.
query is the exception: it only narrows results when it’s the only input. Combined with any fieldFilters, metadataFilters, or additionalPropertyFilters, query stops filtering. Every object matching the other filters is returned, with query-matching ones flagged via matchedFields and ranked ahead of the rest, but non-matching objects are not excluded. To combine a hard-filtered name search with other filters, use fieldFilters with key: "name" (per-word match, not phrase-anchored, see Name filter vs. name query below) or searchNameQuery (exact or contains match, the most literal option) instead. There is currently no way to combine a hard-filtered free-text search across metadata with other filters. query is the only parameter that searches metadata values without requiring you to know the exact key first, and it does not filter when combined with anything else. metadataFilters also searches metadata, but only under a key you specify.
Type filter combined with a numeric metadata range (POIs with a recorded inspection pressure between 80 and 120):
GET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"type","values":["poi"]} &metadataFilters={"key":"inspection.pressure","values":["range:[80..120]"]}Asset type combined with a name filter, newest updates first:
GET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"asset-type","values":["pump"]} &fieldFilters={"key":"name","values":["vibration"]} &sortBy=updatedAt&sortDir=DESCThis means: objects whose asset type is pump and whose name contains a word starting with “vibration” (anywhere in the name, not only at the start), sorted by most recently updated.
Two fieldFilters on the same key (OR’d) combined with a fieldFilters on a different key (AND’d): POIs or zones, restricted to the pump asset type:
GET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"type","values":["poi","zone"]} &fieldFilters={"key":"asset-type","values":["pump"]}A fieldFilters type restriction combined with an additionalPropertyFilters match and a metadataFilters match: POIs with the fire-extinguisher icon whose inspection status records a “warning”:
GET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"type","values":["poi"]} &additionalPropertyFilters={"key":"icon","values":["fire-extinguisher"]} &metadataFilters={"key":"inspection.status","values":["warning"]}A fully loaded request combining all four filter inputs, a dedicated name filter, a date range, pagination, and sort. This shows the full request shape:
GET {api_url}/v1/twin/{contextId}/search ?query=pressure &fieldFilters={"key":"type","values":["poi","zone"]} &metadataFilters={"key":"inspection.pressure","values":["range:[80..120]"]} &additionalPropertyFilters={"key":"icon","values":["fire-extinguisher"]} &searchNameQuery=Pump&nameMatch=contains &updatedAfter=2026-01-01T00:00:00&updatedBefore=2026-12-31T23:59:59 &page=1&limit=25&sortBy=updatedAt&sortDir=DESCSorting
Section titled “Sorting”sortBy | Sorts by |
|---|---|
name | Object name |
createdAt | Creation timestamp |
updatedAt | Last-updated timestamp |
Each sortBy value combines with either sortDir value:
GET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"type","values":["poi"]} &sortBy=name&sortDir=ASCGET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"type","values":["poi"]} &sortBy=createdAt&sortDir=DESCGET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"type","values":["poi"]} &sortBy=updatedAt&sortDir=DESCsortDir defaults to ASC when omitted. sortBy has no documented default field. Omitting it sorts by internal relevance score (descending), with id (ascending) as a tie-breaker. Only query and fieldFilters with key: "name" affect that score; every other filter is a pure yes/no match with no effect on ranking, so in practice, omitting sortBy without query or a name filter returns results in id order. This is an implementation detail, not a guaranteed contract. Set sortBy explicitly whenever a specific field order matters to your integration.
Numeric and range filters
Section titled “Numeric and range filters”The full comparison and range syntax for metadataFilters and additionalPropertyFilters values on numeric-typed properties:
| Operator | Example | Meaning |
|---|---|---|
= | =123 | Equal to |
!= | !=123 | Not equal to |
> | >123 | Greater than |
>= | >=123 | Greater than or equal to |
< | <123 | Less than |
<= | <=123 | Less than or equal to |
range:[a..b] | range:[80..120] | Inclusive on both bounds |
range:(a..b) | range:(80..120) | Exclusive on both bounds |
range:[a..b) | range:[80..120) | Inclusive lower, exclusive upper |
range:(a..b] | range:(80..120] | Exclusive lower, inclusive upper |
Integers, decimals, negative numbers, and scientific notation are all supported, e.g. >=-40, =3.14, or range:[1.5e+35..2.0e+35].
Name filter vs. name query
Section titled “Name filter vs. name query”There are three distinct ways to match by name or text, and they behave differently:
| Parameter | Match style | Scope | Combines with other filters? |
|---|---|---|---|
query | Fuzzy (typo-tolerant) + per-word prefix match | Name and metadata string values | No, only narrows results when used alone |
fieldFilters={"key":"name",...} | Per-word OR match, last word as prefix, not phrase-anchored | Name only | Yes, always ANDs |
searchNameQuery + nameMatch | contains (default) or exact, no fuzziness | Name only | Yes, always ANDs |
Use query for a standalone free-text search that should also surface metadata hits. It only narrows results on its own; add any other filter and query stops excluding non-matches (see Combining filters above). Use fieldFilters with key: "name" when a per-word match that combines under the same AND/OR rules as type or asset-type is good enough. Use a single, distinctive word for the most precise result, since a multi-word value ORs each word independently rather than matching the phrase. Use searchNameQuery with nameMatch when you need a precise substring or exact-name lookup with no fuzzy tolerance and no per-word looseness, for example to validate that a name exists verbatim.
Response
Section titled “Response”{ "results": [ { "id": "5e1c0b3a-2d9a-4b7d-8f0b-9a2c8f1b7a10", "name": "Fire extinguisher 12", "type": "Poi", "matchedFields": [ { "matchKey": "name", "highlight": "Fire <em>extinguisher</em> 12" } ], "createdAt": "2026-06-01T10:00:00.000Z", "updatedAt": "2026-06-01T10:00:00.000Z", "createdById": "8f0b9a2c-8f1b-4a10-9e1c-0b3a2d9a4b7d" } ], "count": 1}matchedFields shows which field or metadata property matched, with the matched text wrapped in <em>. matchKey is either a root field name (e.g. name) or @<metadata-path> for a matched nested metadata or property value.
Highlighting details
Section titled “Highlighting details”A single result can carry multiple matchedFields entries, one per field or metadata property that matched the query. For example, a combined query + metadataFilters request that matches both the object’s name and an inspection metadata value returns:
{ "id": "5e1c0b3a-2d9a-4b7d-8f0b-9a2c8f1b7a10", "name": "Pressure gauge 04", "type": "Poi", "matchedFields": [ { "matchKey": "name", "highlight": "<em>Pressure</em> gauge 04" }, { "matchKey": "@inspection.status", "highlight": "<em>Warning</em>: recalibration due" } ], "createdAt": "2026-06-01T10:00:00.000Z", "updatedAt": "2026-06-01T10:00:00.000Z", "createdById": "8f0b9a2c-8f1b-4a10-9e1c-0b3a2d9a4b7d"}Highlight fragments are HTML-escaped before the <em> markers are inserted, so any <, >, or & in the matched text arrives as <, >, or & rather than raw markup. This is safe to render directly in HTML without a second escaping pass, but decode it first if you need the plain-text value.
Edge cases
Section titled “Edge cases”| Situation | Behavior |
|---|---|
No query and no filter arrays (or all empty) | Returns every business object in the twin, up to limit, with no ranking (match_all). |
An unrecognized fieldFilters key | Silently ignored. The filter contributes nothing to the query and does not raise a 400. |
A metadataFilters/additionalPropertyFilters value of * or "" | Skipped. If every value in that filter’s values array is skipped this way, the filter still requires the object to have a property at that key. It becomes a key-presence check with no value constraint. |
Examples
Section titled “Examples”Free-text search across name and metadata. Only narrows results when used alone (see Combining filters):
GET {api_url}/v1/twin/{contextId}/search ?query=extinguisherName word-prefix search. Matches any name containing a word starting with “extinguisher” (e.g. “Fire Extinguisher 12”), not only names starting with it:
GET {api_url}/v1/twin/{contextId}/search ?fieldFilters={"key":"name","values":["extinguisher"]}Name-contains search sorted by name:
GET {api_url}/v1/twin/{contextId}/search ?searchNameQuery=Pump&nameMatch=contains&sortBy=name&sortDir=ASCError cases
Section titled “Error cases”| Status | Cause |
|---|---|
400 Bad Request | A fieldFilters, metadataFilters, or additionalPropertyFilters value is not valid JSON, or does not match the { key, values } shape. |
400 Bad Request | limit is less than 1 or greater than 50, or page is less than 1. |
400 Bad Request | nameMatch, sortBy, or sortDir is set to a value outside its documented list. |
400 Bad Request | createdBefore, createdAfter, updatedBefore, or updatedAfter doesn’t match the YYYY-MM-DDTHH:mm:ss format. |
400 Bad Request | createdById is not a valid UUID. |
Full example
Section titled “Full example”Repeated filter parameters (multiple fieldFilters, metadataFilters, or additionalPropertyFilters with different JSON values) can’t be built with requests’ dict-based params, since a dict can only hold one value per key. Build the query string yourself with urllib.parse.urlencode and a list of tuples instead:
import jsonimport requestsfrom urllib.parse import urlencode
API_URL = "https://<your-regional-api-url>"TOKEN = "<access_token>"CONTEXT_ID = "<twin-id>"
headers = {"Authorization": f"Bearer {TOKEN}"}
params = [ ("fieldFilters", json.dumps({"key": "type", "values": ["poi"]})), ("metadataFilters", json.dumps({"key": "inspection.pressure", "values": ["range:[80..120]"]})), ("sortBy", "name"), ("sortDir", "ASC"),]
response = requests.get( f"{API_URL}/v1/twin/{CONTEXT_ID}/search?{urlencode(params)}", headers=headers,).json()
for result in response["results"]: print(result["type"], result["name"], result["id"])Searching nodes
Section titled “Searching nodes”GET /v1/nodes/search searches your organization’s node hierarchy (divisions, sites, folders, twins, asset libraries, and data files) rather than the business objects inside a twin. The organization is resolved from the access token, not passed as a parameter.
Query parameters
Section titled “Query parameters”| Parameter | Type | Notes |
|---|---|---|
nodeTypes | array of DataNodeType | Filter to one or more node types. Repeat the parameter, e.g. ?nodeTypes=Site&nodeTypes=Folder. Values: Organization, Site, Pointcloud, Mesh, SiteFile, Folder, Project, Division, Artifact, DataBundle, Twin, AssetLibrary. |
excludeNodeTypes | array of DataNodeType | Exclude one or more node types. Same repeat syntax. |
searchNameQuery | string | Filter by node name. |
nameMatch | exact | contains | Match mode for searchNameQuery. Default contains. |
parentId | uuid | Direct children of this node only. |
ancestorId | uuid | Descendants at any depth, excluding the ancestor itself. Use this for nested content under a site or folder; use parentId for direct children only. |
createdById | uuid | Filter by creator. |
createdBefore / createdAfter | timestamp | Strictly before/after. Format YYYY-MM-DDTHH:mm:ss. |
updatedBefore / updatedAfter | timestamp | Same format. |
bytesStored | integer | Exact match. |
minBytesStored / maxBytesStored | integer | Inclusive bounds. |
childrenCount | integer | Exact match. |
minChildrenCount / maxChildrenCount | integer | Inclusive bounds. |
includesThumbnail | true | false | Include signed thumbnail URLs where available. Default false. |
page | integer | Default 1. |
limit | integer | 1–50, default 20. |
sortBy | name | createdAt | updatedAt | bytesStored | childrenCount | Sort field. |
sortDir | ASC | DESC | Sort direction. Default ASC. |
Response
Section titled “Response”{ "items": [ { "id": "a6f75b3c-f261-4b27-9a2a-9a6cc1234c53", "createdAt": "2026-06-01T10:00:00.000Z", "updatedAt": "2026-06-01T10:00:00.000Z", "name": "Building A", "type": "Site", "createdById": "8f0b9a2c-8f1b-4a10-9e1c-0b3a2d9a4b7d", "parentId": "d2a1c4b7-3e10-4f2c-9a6b-7c1d8e2f3a45", "childrenCount": 4, "bytesStored": 15728640, "thumbnailSignedUrl": null } ], "total": 1}Examples
Section titled “Examples”All sites under an ancestor node:
GET {api_url}/v1/nodes/search ?nodeTypes=Site&ancestorId=d2a1c4b7-3e10-4f2c-9a6b-7c1d8e2f3a45Folders whose name contains “Archive”, sorted by name:
GET {api_url}/v1/nodes/search ?nodeTypes=Folder&searchNameQuery=Archive&nameMatch=contains&sortBy=name&sortDir=ASCimport requestsfrom urllib.parse import urlencode
API_URL = "https://<your-regional-api-url>"TOKEN = "<access_token>"ANCESTOR_ID = "<division-or-site-id>"
headers = {"Authorization": f"Bearer {TOKEN}"}
params = [ ("nodeTypes", "Site"), ("ancestorId", ANCESTOR_ID), ("sortBy", "name"), ("sortDir", "ASC"),]
response = requests.get( f"{API_URL}/v1/nodes/search?{urlencode(params)}", headers=headers,).json()
for node in response["items"]: print(node["type"], node["name"], node["id"])