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 is | Stable across | Use when | |
|---|---|---|---|
id | Internal UUID assigned by the platform | Everything except a replace re-import | The building has no BIM model, or you have already resolved the id |
externalId | The identifier from the system the node came from — the IFC GlobalId when the building was imported from BIM | Re-imports, because the GlobalId comes from the model | The 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
| API | Accepts |
|---|---|
Data layer values (PUT …/values) | Either — keyBy: "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 / elements | Your 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:
GET /api/v1/buildings/{buildingId}/spaces?limit=1000&offset=0Each 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 GlobalId — externalId is null. Two options:
- Use internal ids: upload layer values with
keyBy: "id". - Stamp your own external ids: when you create the space (
POST /api/v1/buildings/{buildingId}/spaces) pass your ownexternalIdtogether with asourceSystemof your choosing ("acme-fm"). From then onkeyBy: "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.reason | Meaning | Fix |
|---|---|---|
BUILDING_HAS_NO_SPACES | The graph is empty — nothing was imported or created | Import the model, or create the rooms |
BUILDING_HAS_NO_EXTERNAL_IDS | Rooms exist but none has a GlobalId | Switch to keyBy: "id", or stamp external ids |
IDENTIFIERS_NOT_IN_BUILDING | Both populated, but these ids are not from this building | Check 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.