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

Devices and profiles API

REST routes for devices, device credentials, connection details, the device log and device profiles, with parameters, answers and errors.

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

MethodPathPermissionDoes
GET/api/iot/devicesiot_device.devices.readList devices
POST/api/iot/devicesiot_device.devices.createCreate a device and its access token
GET/api/iot/devices/{deviceId}iot_device.devices.readRead one device
PATCH/api/iot/devices/{deviceId}iot_device.devices.updateChange name, label, type, profile, status, additional info
DELETE/api/iot/devices/{deviceId}iot_device.devices.deleteDelete a device
GET/api/iot/devices/{deviceId}/connectioniot_device.devices.readWhere the device connects
GET/api/iot/devices/{deviceId}/connectionsiot_device.devices.readThe connection log
GET/api/iot/devices/{deviceId}/logiot_device.devices.readConnection log and audit rows, merged

List devices

GET /api/iot/devices (pages from 1)

ParameterValues
pagefrom 1 (default 1)
pageSize1 to 100 (default 20)
searchup to 128 characters: name, label or serial
deviceProfileIda profile id
statusACTIVE, QUARANTINED or DISABLED
isGatewaytrue or false
onlinetrue, false or unknown
sortname, 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, pageSize up to 100).
  • GET .../log?page=1&pageSize=50&source=connection|audit: the connection log and the device’s audit rows merged, newest first. page × pageSize may be at most 1,000.

Device credentials

MethodPathDoes
GET/api/iot/devices/{deviceId}/credentialsThe active credential, without its secret
PUT/api/iot/devices/{deviceId}/credentialsSwitch to another credential
POST/api/iot/devices/{deviceId}/credentials/rotateA new secret of the same type (201)
DELETE/api/iot/devices/{deviceId}/credentialsRevoke 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

MethodPathPermissionDoes
GET/api/iot/device-profilesiot_device_profile.profiles.readList (page from 1, pageSize, search, transportType, onlineRule, sort)
POST/api/iot/device-profilesiot_device_profile.profiles.writeCreate
GET/api/iot/device-profiles/{profileId}.profiles.readRead
PATCH/api/iot/device-profiles/{profileId}.profiles.writeChange; send expectedVersion
DELETE/api/iot/device-profiles/{profileId}.profiles.writeDelete
POST/api/iot/device-profiles/{profileId}/default.profiles.writeMake it the default
POST/api/iot/device-profiles/{profileId}/duplicate.profiles.writeCopy it as “ (copy)”
GET/api/iot/device-profiles/{profileId}/key-usage?keys=a,b.profiles.readWho reads these keys (rules, boards, flows)
GET, PUT, POST/api/iot/device-profiles/{profileId}/provisioningiot_device_profile.provisioning.manageProvisioning 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:

FieldRange
transportTypeMQTT, HTTP, ANY, or a registered protocol
allowedAuthModes1 to 3 of ACCESS_TOKEN, MQTT_BASIC, X509_CERTIFICATE
mqttTransportTLS_REQUIRED (default) or PLAINTEXT_ALLOWED
allowedNetworksup to 64 networks or addresses
payloadMaxBytes1,024 to 65,536
downlinkMaxBytes1,024 to 65,536
inactivityTimeoutSec10 to 604,800
maxGatewayChildren0 to 10,000
rpcTimeoutDefaultSec1 to 300
onlineWindowFactor1.5 to 10, one decimal
telemetrySchema, cfgSchemaup to 500 keys each
rateLimitscount: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:

ReasonMeaning
stale_profileSomeone saved the profile since you read it (409)
name_takenAnother profile has this name
default_profileThe default profile cannot be deleted
builtin_profileA built-in profile cannot be changed this way
still_referencedDevices or other records still use the profile
reference_unverifiableA module that may use the profile could not be asked; try again