Skip to content

Identity: finding the right room

Every integration on these pages comes down to one question: given a record in your system, which node of the building graph does it belong to? Get this right and the rest is plumbing. Get it wrong and you will see 200 OK with written: 0, or values landing on the wrong floor.

The graph, briefly

site → building → storey → space (room) → element (equipment) → point (a sensor reading)

Data layers attach to spaces. Tickets point at a spaceId, storeyId or elementId. Telemetry attaches to a point, which belongs to an element, which sits in a space. Same tree, different depth.

Two identifiers per node

What it isStable acrossUse when
idInternal UUID assigned by the platformEverything except a replace re-importThe building has no BIM model, or you have already resolved the id
externalIdThe identifier from the system the node came from — the IFC GlobalId when the building was imported from BIMRe-imports, because the GlobalId comes from the modelThe building came from BIM and your data is keyed to the model

Each node also carries sourceSystem, the provenance tag: ifc for anything the IFC import created. Uniqueness is on the triple (buildingId, sourceSystem, externalId) — which means your system can stamp its own externalId under its own sourceSystem without colliding with the IFC identifiers already there.

There is also code ("B1-S02-SP003"), a human-facing machine-readable label, unique per building. Useful in exports; it is not a lookup key in the API.

Why GlobalId is the safer join key for a BIM building

Internal ids are recreated when a model is re-imported with replace: true. IFC GlobalIds are not — they come from the model itself and survive the round trip. If a building came from BIM, key your integration on externalId and you will not have to re-resolve everything after the next model revision.

Which identifier each API wants

APIAccepts
Data layer values (PUT …/values)EitherkeyBy: "externalId" (default, GlobalIds) or keyBy: "id" (internal UUIDs)
Tickets (POST /requests)Internal UUIDs only — spaceId, storeyId, elementId
Telemetry (POST …/observations)pointId — an internal UUID that must already exist in this building
Creating spaces / elementsYour own externalId + sourceSystem, if you want them

Data layers are the forgiving one: hand it GlobalIds and it resolves them for you. That is why "a room id and some data" is genuinely the whole payload for a layer, and why a layer is the fastest first integration to stand up.

Building the map

Anything that needs internal UUIDs — tickets above all — needs a GlobalId → id map. Build it once and cache it:

http
GET /api/v1/buildings/{buildingId}/spaces?limit=1000&offset=0

Each row carries id, externalId, code, name, its storey and its space type, so one pass gives you every join key you need.

There is no server-side lookup by externalId

The spaces list filters by storeyId, spaceTypeSlug, status and isLeasable — not by externalId or code. You cannot ask "which room is GlobalId X"; you page the building and build the index client-side. For a building with a few thousand rooms that is a handful of requests, once, at startup.

Page size: current tv-api allows limit up to 1000. If you get a 400, the deployment you are talking to still caps it at 100 — fall back rather than failing.

Refresh the map after a model re-import. Values keyed by GlobalId survive one; cached internal UUIDs may not.

When the building has no BIM model

Plenty of buildings do not have one. Their rooms are drawn in the map editor or created through the API, and they have no GlobalIdexternalId is null. Two options:

  1. Use internal ids: upload layer values with keyBy: "id".
  2. Stamp your own external ids: when you create the space (POST /api/v1/buildings/{buildingId}/spaces) pass your own externalId together with a sourceSystem of your choosing ("acme-fm"). From then on keyBy: "externalId" matches your identifiers directly, and you never keep a mapping table at all.

Option 2 is the better shape when your system is the one that knows the room inventory.

When it does not match

A values upload that matches nothing tells you which of the three causes it is:

diagnostics.reasonMeaningFix
BUILDING_HAS_NO_SPACESThe graph is empty — nothing was imported or createdImport the model, or create the rooms
BUILDING_HAS_NO_EXTERNAL_IDSRooms exist but none has a GlobalIdSwitch to keyBy: "id", or stamp external ids
IDENTIFIERS_NOT_IN_BUILDINGBoth populated, but these ids are not from this buildingCheck the buildingId — usually a stale id in a config, or the identifiers came from a different model of the same site

A partial match is not an error: the response's unmatched array lists exactly the keys that found no room, and the rest are written. Log it, alert on a threshold, and you will catch the day a floor is renumbered.

One edge worth knowing: a single room can be referenced under more than one GlobalId when a site carries several discipline models. If two identifiers in the same payload resolve to the same room, the last one written wins — the upload does not error, so keep one identifier per room in one payload.

Built on the Tango Vision platform. Questions? developers@tango.vision