These routes manage devices and device profiles from code. Paths are under https://<your host>/tenant/<tenantId>. Conventions (paging, strict bodies, times) are in REST API basics.
Devices
| Method | Path | Permission | Does |
|---|---|---|---|
| GET | /api/iot/devices | iot_device.devices.read | List devices |
| POST | /api/iot/devices | iot_device.devices.create | Create a device and its access token |
| GET | /api/iot/devices/{deviceId} | iot_device.devices.read | Read one device |
| PATCH | /api/iot/devices/{deviceId} | iot_device.devices.update | Change name, label, type, profile, status, additional info |
| DELETE | /api/iot/devices/{deviceId} | iot_device.devices.delete | Delete a device |
| GET | /api/iot/devices/{deviceId}/connection | iot_device.devices.read | Where the device connects |
| GET | /api/iot/devices/{deviceId}/connections | iot_device.devices.read | The connection log |
| GET | /api/iot/devices/{deviceId}/log | iot_device.devices.read | Connection log and audit rows, merged |
List devices
GET /api/iot/devices (pages from 1)
| Parameter | Values |
|---|---|
page | from 1 (default 1) |
pageSize | 1 to 100 (default 20) |
search | up to 128 characters: name, label or serial |
deviceProfileId | a profile id |
status | ACTIVE, QUARANTINED or DISABLED |
isGateway | true or false |
online | true, false or unknown |
sort | name, connection, lastSeen, createdAt, with :asc or :desc |
{
"devices": [
{
"deviceId": "6f1c…",
"name": "pump-station-04",
"label": "Pump station 4",
"type": "pump-controller",
"serial": null,
"deviceProfileId": "a1b2…",
"status": "ACTIVE",
"isGateway": false,
"gatewayDeviceId": null,
"model": null,
"fwTitle": null,
"fwVersion": null,
"online": true,
"active": true,
"lastConnectAt": "2026-11-08T06:12:03.000Z",
"lastDisconnectAt": null,
"lastActivityAt": "2026-11-09T09:41:10.000Z",
"lastRemoteIp": "203.0.113.20",
"lastTransport": "mqtts",
"additionalInfo": {},
"createdAt": "2026-10-01T08:00:00.000Z",
"updatedAt": "2026-11-08T06:12:03.000Z"
}
],
"total": 1
}
online is null on a profile whose online rule is Last seen only. GET /api/iot/devices/{deviceId} answers { "device": {...} } with the same fields plus profileName and profileTransport.
Create a device
POST/tenant/{tenantId}/api/iot/devices
Fields: name (required, 1 to 128 characters, unique), label (up to 255), type (up to 64), deviceProfileId (else the default profile), isGateway, additionalInfo (an object of up to 50 keys and 8 KiB). The answer holds the device and, for profiles whose devices log in themselves, an access token in credential.secret.token. The token is shown in this answer only.
Request body application/json
{
"name": "pump-station-04",
"label": "Pump station 4",
"deviceProfileId": "a1b2…"
}
Example curl
curl -X POST 'https://<your-host>/tenant/{tenantId}/api/iot/devices' \
-H 'Content-Type: application/json' \
-d '{"name":"pump-station-04","label":"Pump station 4","deviceProfileId":"a1b2…"}'
Responses
201Created
{
"device": {
"deviceId": "6f1c…",
"name": "pump-station-04",
"status": "ACTIVE",
"…": "…"
},
"credential": {
"credential": {
"credentialId": "…",
"type": "ACCESS_TOKEN",
"…": "…"
},
"secret": {
"token": "Xq3…"
}
}
}
403Forbidden
{
"message": "The device limit of this account is reached.",
"code": "FORBIDDEN",
"details": {
"key": "max_devices",
"limit": 43,
"current": 43,
"adding": 1
}
}
409Conflict
{
"message": "A device with this name already exists.",
"code": "CONFLICT",
"details": {
"reason": "name_taken"
}
}
Change a device
PATCH /api/iot/devices/{deviceId} takes any of name, label, type, deviceProfileId, isGateway, additionalInfo, status (ACTIVE or DISABLED). Send only what changes. A quarantined device is approved through provisioning, not here: “A quarantined device is activated through provisioning, not edited here.” A new name, a new profile or disabling ends the device’s live session; it reconnects under the new settings.
Delete a device
DELETE answers { "message": "Device deleted." }. The device’s readings are removed in the background. A device that is still used is refused with 409, for example “The device is still in use by 1 asset placement or relation (iot_asset).” Take it off its asset and remove its relations first: see Placing devices and Relations.
Connection details
GET /api/iot/devices/{deviceId}/connection returns nothing secret:
{
"connection": {
"mqtt": { "host": "mqtt.example.com", "port": 8883, "tls": true },
"httpBase": "https://acme.example.com",
"topics": { "telemetry": "v1/telemetry", "attributes": "v1/attributes", "rpcRequest": "v1/rpc/req/+", "rpcResponse": "v1/rpc/res/{id}" },
"telemetryKeys": [{ "key": "temperature", "type": "number" }]
}
}
mqtt is null when the profile refuses MQTT on that listener; rpcRequest and rpcResponse are null on the Legacy compatible scheme.
Logs
GET .../connections?page=1&pageSize=50: connects and disconnects, newest first (pages from 1,pageSizeup to 100).GET .../log?page=1&pageSize=50&source=connection|audit: the connection log and the device’s audit rows merged, newest first.page×pageSizemay be at most 1,000.
Device credentials
| Method | Path | Does |
|---|---|---|
| GET | /api/iot/devices/{deviceId}/credentials | The active credential, without its secret |
| PUT | /api/iot/devices/{deviceId}/credentials | Switch to another credential |
| POST | /api/iot/devices/{deviceId}/credentials/rotate | A new secret of the same type (201) |
| DELETE | /api/iot/devices/{deviceId}/credentials | Revoke the credential |
All need iot_device.credentials.manage. Switch, rotate and revoke end the old credential at once (there is no grace window) and disconnect the device.
PUT bodies:
{ "type": "ACCESS_TOKEN" }
{ "type": "MQTT_BASIC", "username": "pump04", "clientId": "pump04", "password": null }
{ "type": "X509_CERTIFICATE", "certificateId": "…" }
For MQTT_BASIC, leave password out (or null) to get a generated one. A chosen password needs at least 16 characters, varied, not containing the user name or client id. The user name provision is reserved. The answer is { "credential": {...}, "secret": { "token": "…" } } or { ..., "secret": { "password": "…" } }, shown once; an X.509 credential has no secret. An X.509 credential is rotated by attaching a new certificate: “An X.509 credential is rotated by attaching a new certificate.” See Device credentials.
Device profiles
| Method | Path | Permission | Does |
|---|---|---|---|
| GET | /api/iot/device-profiles | iot_device_profile.profiles.read | List (page from 1, pageSize, search, transportType, onlineRule, sort) |
| POST | /api/iot/device-profiles | iot_device_profile.profiles.write | Create |
| GET | /api/iot/device-profiles/{profileId} | .profiles.read | Read |
| PATCH | /api/iot/device-profiles/{profileId} | .profiles.write | Change; send expectedVersion |
| DELETE | /api/iot/device-profiles/{profileId} | .profiles.write | Delete |
| POST | /api/iot/device-profiles/{profileId}/default | .profiles.write | Make it the default |
| POST | /api/iot/device-profiles/{profileId}/duplicate | .profiles.write | Copy it as “ |
| GET | /api/iot/device-profiles/{profileId}/key-usage?keys=a,b | .profiles.read | Who reads these keys (rules, boards, flows) |
| GET, PUT, POST | /api/iot/device-profiles/{profileId}/provisioning | iot_device_profile.provisioning.manage | Provisioning settings; POST rotates or revokes a provisioning key |
Sort fields: name, transport, online, updated. A list answers { "profiles": [...], "total": n }; one profile { "profile": {...} }.
Create a profile
A new profile needs a name (up to 128 characters) and an onlineRule: LIVE_SOCKET (Live connection), UPLINK_WINDOW (Recent message, also needs expectedIntervalS, 60 to 604,800) or LAST_SEEN_ONLY (Last seen only). Every other field takes its default.
{
"name": "Cold room sensor",
"onlineRule": "UPLINK_WINDOW",
"expectedIntervalS": 300,
"transportType": "MQTT",
"telemetrySchema": [
{ "key": "temperature", "type": "number", "unit": "°C", "label": "Temperature", "min": -30, "max": 10, "decimals": 1 }
]
}
Selected fields and their ranges:
| Field | Range |
|---|---|
transportType | MQTT, HTTP, ANY, or a registered protocol |
allowedAuthModes | 1 to 3 of ACCESS_TOKEN, MQTT_BASIC, X509_CERTIFICATE |
mqttTransport | TLS_REQUIRED (default) or PLAINTEXT_ALLOWED |
allowedNetworks | up to 64 networks or addresses |
payloadMaxBytes | 1,024 to 65,536 |
downlinkMaxBytes | 1,024 to 65,536 |
inactivityTimeoutSec | 10 to 604,800 |
maxGatewayChildren | 0 to 10,000 |
rpcTimeoutDefaultSec | 1 to 300 |
onlineWindowFactor | 1.5 to 10, one decimal |
telemetrySchema, cfgSchema | up to 500 keys each |
rateLimits | count:seconds[,count:seconds] per family, for example "telemetry_msgs": "10:1,300:60" |
Read an existing profile first to see every field with its current value; the profile editor covers the same fields with explanations, see Device profiles.
Refusals
A body that breaks a field rule is a 400 with the problems in issues, each with its path and message, for example online_rule_required (a new profile needs an online rule), expected_interval_required (Recent message needs expectedIntervalS) or online_rule_not_allowed (the online rule does not fit the transport).
Other refusals carry details.reason, for example:
| Reason | Meaning |
|---|---|
stale_profile | Someone saved the profile since you read it (409) |
name_taken | Another profile has this name |
default_profile | The default profile cannot be deleted |
builtin_profile | A built-in profile cannot be changed this way |
still_referenced | Devices or other records still use the profile |
reference_unverifiable | A module that may use the profile could not be asked; try again |

