New: the AI firewall watches every module and explains each incident in plain words. See how it works

Sites and assets API

Build and read the tree of sites and assets, move assets, place devices on them and relate assets and devices over REST – fields, limits and errors.

These routes manage the asset tree of Hermetry Twin: sites, buildings, floors, rooms, lines, machines and your own kinds, where each device is placed, and the relations between assets and devices. The concepts are in Asset hierarchy. Paths are under https://<your host>/tenant/<tenantId>.

Routes

MethodPathPermissionDoes
GET/api/iot/assetsiot_asset.assets.readList as a flat page, a tree, the Sites & Assets table or its summary
POST/api/iot/assetsiot_asset.assets.manageCreate an asset
GET/api/iot/assets/{assetId}iot_asset.assets.readOne asset with its ancestors and counts
PATCH/api/iot/assets/{assetId}iot_asset.assets.manageChange an asset
DELETE/api/iot/assets/{assetId}iot_asset.assets.manageDelete an asset
POST/api/iot/assets/{assetId}/moveiot_asset.assets.manageMove under another parent
GET/api/iot/assets/{assetId}/devicesiot_asset.assets.readDevices placed on it (and below)
POST/api/iot/assets/{assetId}/devicesiot_asset.relations.managePlace up to 100 devices on it
GET/api/iot/devices/{deviceId}/placementiot_asset.assets.readWhere a device is
PUT/api/iot/devices/{deviceId}/placementiot_asset.relations.managePlace a device, or remove its placement
GET/api/iot/relationsiot_asset.assets.readList relations
POST/api/iot/relationsiot_asset.relations.manageCreate a relation
DELETE/api/iot/relations/{relationId}iot_asset.relations.manageDelete a relation

Default roles: reading is a User permission; changing assets, placements and relations is an Admin permission. Every change is written to the audit log (the changed field names, never the values).

The asset

FieldRules
namerequired, 1 to 255 characters
kindrequired; letters, digits and _, 1 to 24, stored in capitals. Suggested: SITE, BUILDING, FLOOR, ROOM, AREA, LINE, MACHINE, OTHER; any other is allowed
codeletters, digits, ., _, -; 1 to 32; stored in capitals; unique among your live assets. Left out: made from the name.
parentIdcreate only; null or absent for a top-level asset. Change it later with move.
descriptionup to 1,000 characters
lat, lng−90 to 90, −180 to 180
timezonean IANA name such as Europe/Istanbul
addressJsonup to 12 text fields
attributesup to 100 keys and 16 KiB; keys start with a letter, then letters, digits, ., _, - (up to 64)
defaultDashboardIda dashboard the asset’s Open dashboard button opens

Read-only fields in answers: iotAssetId, path (the codes from the top, for example /IST/BLD-A/GF), depth, createdAt, updatedAt.

POST/tenant/{tenantId}/api/iot/assets

Create an asset. Unknown fields are refused.

Request body application/json

{
    "name": "Compressor room",
    "kind": "ROOM",
    "code": "CR1",
    "parentId": "e5f6…",
    "timezone": "Europe/Istanbul",
    "attributes": {
        "area_m2": 42
    }
}

Example curl

curl -X POST 'https://<your-host>/tenant/{tenantId}/api/iot/assets' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Compressor room","kind":"ROOM","code":"CR1","parentId":"e5f6…","timezone":"Europe/Istanbul","attributes":{"area_m2":42}}'

Responses

201Created
{
    "asset": {
        "iotAssetId": "77aa…",
        "parentId": "e5f6…",
        "path": "/IST/BLD-A/GF/CR1",
        "depth": 4,
        "code": "CR1",
        "name": "Compressor room",
        "kind": "ROOM",
        "timezone": "Europe/Istanbul",
        "attributes": {
            "area_m2": 42
        },
        "…": "…"
    }
}
409Conflict
{
    "message": "Another asset already uses this code.",
    "code": "CONFLICT",
    "details": {
        "reason": "code_taken"
    }
}

PATCH takes the same fields except parentId. A new code rewrites the paths of every asset below in the same step.

GET /api/iot/assets/{assetId} adds ancestors (root first, each with id, code, name, kind), effectiveTimezone (its own, else the nearest ancestor’s), childCount and deviceCount.

List and tree

GET /api/iot/assets with view:

viewAnswerParameters
list (default){ assets, total, page, pageSize }, a flat pageparentId (a UUID or root), subtreeOf, kind, search, includeDeleted, page from 1, pageSize up to 200 (default 50)
treeThe nested treerootId, depth (default 6, at most 12); at most 5,000 assets
tableOne page of the Sites & Assets table: each asset with its place, children, devices and active alarmssearch, kind, site (a subtree), parentId, alarmed=true, sort (name, kind, location, children, devices, alarms with :asc or :desc), page, pageSize
summaryThe counts and the sites with their alarms per severity

Alarm figures are included only when the caller may read alarms.

Move

POST /api/iot/assets/{assetId}/move
{ "parentId": "c3d4…" }

null makes it a top-level asset. Everything below moves with it. Moving an asset under itself or one of its descendants is refused: “An asset cannot be moved under itself or one of its own descendants.” (400, cycle). A path may be at most 1,024 characters (too_deep).

Delete

An asset with child assets is refused: “This asset still has child assets: move or delete them first.” (409, has_children). If another module reports that it still uses the asset, the delete is refused with 409 naming that module. Deleting an asset ends its device placements and relations; the devices themselves stay.

Place devices

POST /api/iot/assets/{assetId}/devices
{ "deviceIds": ["6f1c…", "8b2d…"] }

Each device has at most one place: placing it here moves it from where it was. The answer { "changes": [...] } lists what moved. One device at a time:

PUT /api/iot/devices/{deviceId}/placement
{ "iotAssetId": "77aa…" }      // or null to remove the placement

Both GET and PUT of .../placement answer { "placement": { "iotAssetId": "77aa…", "path": [ { "iotAssetId": "…", "code": "IST", "name": "Istanbul plant", "kind": "SITE" }, … ] } }, root first. A device with no place has iotAssetId: null.

GET /api/iot/assets/{assetId}/devices lists devices placed on the asset and, by default, below it (recursive=false for the asset alone): search, online=true|false, sort=name|connection:asc|desc, page from 1, pageSize up to 200. The answer is { items, total, page, pageSize }.

Relations

A relation links two ends, each an ASSET or a DEVICE, with a type: CONTAINS, MANAGES, FEEDS, MONITORS or your own (letters, digits, _, up to 32, stored in capitals).

POST/tenant/{tenantId}/api/iot/relations

Create a relation. additionalInfo holds up to 20 keys and 4 KiB.

Request body application/json

{
    "fromType": "DEVICE",
    "fromId": "8b2d…",
    "toType": "DEVICE",
    "toId": "6f1c…",
    "type": "FEEDS",
    "additionalInfo": {
        "line": "DN150"
    }
}

Example curl

curl -X POST 'https://<your-host>/tenant/{tenantId}/api/iot/relations' \
  -H 'Content-Type: application/json' \
  -d '{"fromType":"DEVICE","fromId":"8b2d…","toType":"DEVICE","toId":"6f1c…","type":"FEEDS","additionalInfo":{"line":"DN150"}}'

Responses

201Created
{
    "relation": {
        "relationId": "…",
        "fromType": "DEVICE",
        "fromId": "8b2d…",
        "toType": "DEVICE",
        "toId": "6f1c…",
        "type": "FEEDS",
        "…": "…"
    }
}
409Conflict
{
    "message": "This relation already exists.",
    "code": "CONFLICT",
    "details": {
        "reason": "relation_exists"
    }
}

Rules:

  • CONTAINS from an asset to a device is the device’s placement (use the placement routes). CONTAINS in any other shape is refused (placement_shape), and an asset inside another asset is set by moving it, not by a relation (parentage_is_move).
  • A relation needs two different ends (self_relation).
  • A missing end is 404 (end_not_found).

GET /api/iot/relations?entityType=ASSET&entityId={assetId} lists the relations of one end in both directions; or filter by fromType, fromId, toType, toId, type. limit 1 to 500 (default 200). The answer is { "relations": [...] }.

Search devices for a picker

GET /api/iot/devices/search?q=pump&limit=20&near=iot_asset:{assetId} answers { "devices": [{ "id", "name", "code", "hint" }] }: at most 20 devices by name, those of the given device or asset first, with hint naming where each is placed. It needs both iot_asset.assets.read and iot_device.devices.read.