This page lists the IoT events a webhook can subscribe to and the data each one carries. The envelope around data, the headers and the signature are described in Webhooks. The list of events offered to your organisation is also available from GET /api/webhooks/events.
General rules:
- Payloads carry ids and safe fields only. A device credential, token, password, certificate or download link is never sent.
- Times are ISO 8601 text in UTC, except alarm times, which are milliseconds since the epoch.
- Events can arrive out of order and, rarely, twice. Deduplicate on
X-Webhook-Deliveryand order by the times in the payload. - The examples show only the envelope’s
eventanddata; the full envelope also haswebhookId,tenantIdandcreatedAt.
Devices
| Event | Sent when |
|---|---|
device.created | A device was added: by hand, from an import, by provisioning or claiming, or as a gateway’s child |
device.updated | The name, label, type, profile, gateway flag, additional info or status changed |
device.deleted | A device was deleted (by hand, by a release, or by undoing an import) |
device.credentials_rotated | The credential was rotated, replaced, renewed (X.509) or revoked |
device.connected | A device came online and stayed online for the hold time |
device.disconnected | A device went offline and stayed offline for the hold time |
{ "event": "device.created", "data": {
"deviceId": "6f1c…", "name": "pump-station-04", "serial": null, "profileId": "a1b2…",
"status": "ACTIVE", "isGateway": false, "createdAt": "2026-11-09T08:00:00.000Z" } }
A gateway’s child also carries gatewayDeviceId.
{ "event": "device.updated", "data": {
"deviceId": "6f1c…", "name": "pump-station-04", "serial": null, "profileId": "a1b2…",
"status": "DISABLED", "isGateway": false, "changedFields": ["status"], "previousStatus": "ACTIVE",
"updatedAt": "2026-11-09T08:10:00.000Z" } }
changedFields names fields only (sorted: additionalInfo, isGateway, label, name, profileId, status, type), never their values. previousStatus is present only when the status changed. An edit that changes none of these fields sends nothing.
{ "event": "device.deleted", "data": {
"deviceId": "6f1c…", "name": "pump-station-04", "serial": null, "profileId": "a1b2…",
"credentialRevoked": true, "deletedAt": "2026-11-09T08:20:00.000Z" } }
{ "event": "device.credentials_rotated", "data": {
"deviceId": "6f1c…", "name": "pump-station-04", "serial": null, "profileId": "a1b2…",
"change": "rotated", "credentialType": "ACCESS_TOKEN", "previousType": "ACCESS_TOKEN",
"changedAt": "2026-11-09T08:30:00.000Z" } }
change is rotated, replaced, renewed or revoked; after revoked, credentialType is null.
{ "event": "device.connected", "data": {
"deviceId": "6f1c…", "name": "router-17", "profileId": "a1b2…", "transport": "mqtts",
"connectedAt": "2026-11-03T10:00:00.000Z", "remoteIpPrefix": "203.0.113.0/24" } }
{ "event": "device.disconnected", "data": {
"deviceId": "6f1c…", "name": "router-17", "profileId": "a1b2…", "reason": "timeout",
"disconnectedAt": "2026-11-03T12:00:00.000Z", "lastSeenAt": "2026-11-03T11:59:40.000Z", "onlineSeconds": 7200 } }
remoteIpPrefix is the /24 of an IPv4 address or the /48 of an IPv6 address, never the full address. reason is clean (the device disconnected), timeout (its keep-alive or uplink window ran out), kicked (the server closed it, for example after a credential change) or quota (it hit a limit).
Hold time and volume. A connection change is sent only after it held for the hold time: Devices > settings > Connection webhooks: hold time (seconds), default 30, 5 to 3,600. A device that drops and comes back within it sends nothing. At most 600 connection events and 600 device change events are sent per organisation per minute by default; the rest of that minute is dropped and your owners and admins get one notice. The devices stay readable through the API.
Alarms
| Event | Sent when |
|---|---|
alarm.created | An alarm opened (a profile rule, a flow block or an operator). Not sent when an open alarm is triggered again. |
alarm.acknowledged | An operator or a flow acknowledged an open alarm. It stays open. |
alarm.cleared | An alarm was resolved: by its rule or flow, by an operator, or because its device was deleted |
{ "event": "alarm.created", "data": {
"alarmId": "9a7e…", "type": "high_temperature", "severity": "CRITICAL",
"originator": { "type": "DEVICE", "id": "6f1c…", "name": "cold-room-2" },
"deviceId": "6f1c…", "profileId": "a1b2…", "url": "/admin/iot/alarms/9a7e…",
"title": "Temperature too high", "startedAt": 1762678800000, "openedAt": 1762678800000,
"details": { "temperature": 9.4 }, "backfilled": false, "metadata": { "topic": null } } }
{ "event": "alarm.acknowledged", "data": {
"alarmId": "9a7e…", "type": "high_temperature", "severity": "CRITICAL", "originator": { "…": "…" },
"deviceId": "6f1c…", "profileId": "a1b2…", "url": "/admin/iot/alarms/9a7e…",
"ackBy": { "userId": "u-55…" }, "ackAt": 1762679100000 } }
{ "event": "alarm.cleared", "data": {
"alarmId": "9a7e…", "type": "high_temperature", "severity": "CRITICAL", "originator": { "…": "…" },
"deviceId": "6f1c…", "profileId": "a1b2…", "url": "/admin/iot/alarms/9a7e…",
"clearedBy": "rule", "reason": "clear", "clearedAt": 1762680000000, "durationSeconds": 1200,
"metadata": { "topic": null } } }
urlis the alarm’s page in the app; put your host and/tenant/<tenantId>in front of it.backfilled: truemarks an alarm opened by late readings (for example after a gateway’s buffer was replayed). Nobody is notified for those.clearedByis"rule"or{ "userId": "…" };reasonisclear,manualororiginator_deleted.metadataisnullwhen a person cleared it.- A change of severity of an open alarm sends no webhook. Alarm webhooks are not throttled; your endpoint’s rate limit still applies.
Commands
| Event | Sent when |
|---|---|
rpc.completed | A command ended SUCCESSFUL (answered, or a one-way command was delivered) |
rpc.failed | A command ended FAILED, TIMEOUT (outcome unknown) or EXPIRED (never reached the device) |
{ "event": "rpc.failed", "data": {
"rpcId": "c0de…", "deviceId": "6f1c…", "method": "pump.start", "status": "TIMEOUT",
"error": null, "source": "api", "completedAt": "2026-11-09T09:00:15.000Z" } }
The answer itself is not in the payload: read it with GET /api/iot/rpc/{rpcId}. A cancelled command sends nothing.
Firmware
| Event | Sent when |
|---|---|
ota.update.initiated | A device was given a firmware package to install |
ota.update.succeeded | A device runs the target version |
ota.update.failed | The update failed, timed out or rolled back |
ota.rollout.wave_completed | A wave passed its gates |
ota.rollout.paused | A gate failed or a person paused the rollout |
ota.rollout.resumed | A person resumed it |
ota.rollout.completed | The last wave passed its gates |
ota.rollout.aborted | A person aborted it, its assignment was removed or replaced, or its package was revoked |
{ "event": "ota.update.failed", "data": {
"deviceId": "6f1c…", "packageId": "f00d…", "title": "router-fw", "version": "2.5.0", "fromVersion": "2.4.1",
"state": "FAILED", "attempts": 1, "at": "2026-11-09T10:00:00.000Z", "stage": "verify", "error": "…" } }
stage is download, verify, update or rollback; error is the device’s code, or a timeout.
{ "event": "ota.rollout.paused", "data": {
"rolloutId": "r-12…", "assignmentId": "as-3…", "packageId": "f00d…", "status": "PAUSED", "wave": 2, "waves": 3,
"at": "2026-11-09T11:00:00.000Z", "reason": "failure_gate", "failures": 4, "failurePct": 6.7, "successPct": 88.3 } }
Rollout payloads carry rolloutId, assignmentId, packageId, status, wave (1-based), waves and at, plus: updated, failures, successPct (wave completed); reason and the counts (paused; reason is failure_gate, success_gate, no_result (no device finished) or manual); pausedReason (resumed); updated (completed); reason (aborted).
Provisioning
| Event | Sent when |
|---|---|
iot.device.claimed | A licensed device was claimed into your organisation |
iot.device.released | A device went back to the factory pool: kind OWNER (you released it), FORCED (platform support) or DEVICE (from the device’s own page) |
{ "event": "iot.device.claimed", "data": {
"deviceId": "6f1c…", "serial": "RT-0017", "model": "…", "claimedAt": "2026-11-09T07:00:00.000Z", "claimedByUserId": "u-55…" } }
{ "event": "iot.device.released", "data": {
"deviceId": "6f1c…", "serial": "RT-0017", "model": "…", "kind": "OWNER", "releasedAt": "2026-11-09T07:30:00.000Z" } }
Rules
| Event | Sent when |
|---|---|
rules.mode_changed | A place was put in Manual, went back to Auto, or its Manual ended on its own |
rules.needs_attention | A rule was switched off because a device or the place it names is gone |
{ "event": "rules.mode_changed", "data": {
"placeType": "asset", "placeId": "77aa…", "placeName": "Greenhouse 2", "mode": "manual", "change": "manual",
"actorId": "u-55…", "until": 1762700000000, "reason": "Harvest" } }
change is manual, auto or expired; actorId is null when a Manual ended by itself. A switch back to Auto also carries applyNow and rearmed.
{ "event": "rules.needs_attention", "data": {
"ruleId": "ru-9…", "name": "Close vents at night", "reason": "…",
"home": { "type": "asset", "id": "77aa…" } } }
Flows
automation.message is sent only by a flow’s Webhook block, and only to the one endpoint the block names (which must subscribe to “Automation message”). It carries topic, payload, originator, flowId, flowVersion, nodeId, actor and the metadata keys the block lists, with X-Idempotency-Key. See Action blocks.
Other events
esim.operation.succeededandesim.operation.failedbelong to eSIM management, which is in development.- API key events (
api_key.created,api_key.updated,api_key.rotated,api_key.deleted,api_key.expired) and organisation events (members, invitations, subscription) are offered too. Their payloads are not covered on this page.

