Skip to main content

Event Catalog

Every webhook delivery contains a single event object. This page documents the event envelope structure and the payload for each event type.

Event Envelope

All events share the same top-level structure:

{
"id": "evt_abc123",
"type": "asset.created",
"organizationId": "o-xxxx",
"created": 1771911526,
"data": {
"object": { },
"previousAttributes": { }
},
"livemode": true
}
FieldTypeDescription
idstringUnique event identifier. Use this for idempotency. Format: evt_ prefix + random string.
typestringThe event type (e.g., asset.created). Determines the shape of data.object.
organizationIdstringThe organization where the event occurred.
createdintegerUnix timestamp (seconds) when the event was generated.
data.objectobjectThe full resource at the time of the event.
data.previousAttributesobjectFor .updated and .status.changed events only. Contains the fields that changed, with their previous values.
isTestbooleanPresent and true only on test events (webhook.test). Absent on real events.
livemodebooleantrue for real events, false for test events.
tip

The previousAttributes field makes it easy to detect exactly what changed without diffing the entire object. For .created events, this field is absent.


Asset Events

asset.created

Fired when a new asset is added to the organization.

{
"id": "evt_ast_001",
"type": "asset.created",
"organizationId": "o-xxxx",
"created": 1771911526,
"data": {
"object": {
"id": "ast-a1b2c3d4",
"name": "HVAC Unit #12",
"assetTypeId": "at-cooling",
"locationId": "loc-building-a",
"status": "Online",
"metadata": {
"manufacturer": "Daikin",
"model": "VRV-IV",
"serialNumber": "DK-2024-00412"
},
"created": 1771911526,
"updated": 1771911526
}
},
"livemode": true
}

asset.telemetry.updated

Fired when an IoT sensor sends new telemetry data for an asset (e.g., temperature, humidity, CO2 level). This is the primary event for real-time monitoring and digital twin integrations.

High frequency

This event fires every time a sensor reading updates the asset record. For assets with sensors reporting every 30 seconds, expect one event per interval. Only subscribe if your integration needs real-time sensor data.

{
"id": "evt_ast_002",
"type": "asset.telemetry.updated",
"organizationId": "o-xxxx",
"created": 1771912000,
"data": {
"object": {
"id": "ast-a1b2c3d4",
"name": "IAQ Sensor - Level 3 Lobby",
"assetTypeId": "at-iaq",
"locationId": "loc-building-a",
"status": "Online",
"connectionStatus": "connected",
"isOnline": true,
"lastTelemetry": {
"temperature": 24.5,
"humidity": 65.2,
"co2": 412,
"pm25": 8.3
},
"lastSeen": 1771912000
},
"previousAttributes": {
"lastTelemetry": {
"temperature": 23.8,
"humidity": 64.1,
"co2": 408,
"pm25": 8.1
},
"lastSeen": 1771911970
}
},
"livemode": true
}

Understanding telemetry fields

The lastTelemetry object contains decoded sensor values — the fields and their names depend on the sensor type (asset type). For example:

Sensor TypeExample Fields
IAQ (Indoor Air Quality)Temperature, Humidity, CO2, PM2Point5, PM10, TVOC, HCHO, Pressure
Water LeakageLeakStatus, BatteryLevel
Temperature & HumidityTemperature, Humidity
Power SocketPower, Voltage, Current, Energy
Door/Window SensorDoorStatus, BatteryLevel

Getting units and labels

Telemetry values are raw numbers (e.g., "CO2": 1761). Units (ppm, °C, %RH) are defined on the asset type, not on each telemetry reading. To get units for display:

# Fetch the asset type definition (cache this — it rarely changes)
curl https://app.infodeck.io/api/v2/organizations/{orgId}/asset-types/{assetTypeId} \
-H "Authorization: Bearer {token}"

The response includes a properties array with unit metadata:

{
"properties": [
{ "key": "Temperature", "name": "Temperature", "type": "float", "unit": "°C" },
{ "key": "Humidity", "name": "Humidity", "type": "float", "unit": "%RH" },
{ "key": "CO2", "name": "CO2", "type": "integer", "unit": "ppm" },
{ "key": "PM2Point5", "name": "PM2.5", "type": "float", "unit": "µg/m³" }
]
}
Integration pattern

Fetch the asset type definition once when you first see an assetTypeId, cache it, and use it to map units to all future telemetry values. The assetTypeId is included in every asset.telemetry.updated event for this purpose.

asset.deleted

Fired when an asset is removed.

{
"id": "evt_ast_003",
"type": "asset.deleted",
"organizationId": "o-xxxx",
"created": 1771913000,
"data": {
"object": {
"id": "ast-a1b2c3d4",
"deleted": true
}
},
"livemode": true
}

asset.status.changed

Fired when an asset's connectivity or operational status changes. The connectionStatus field is a boolean (true = online, false = offline) computed from the device's last uplink timestamp and its keepAlive interval. Infodeck checks connectivity every 2 minutes.

{
"id": "evt_abc123",
"type": "asset.status.changed",
"organizationId": "o-xxxx",
"streamRecordId": "rec_abc123",
"created": 1771914000,
"data": {
"object": {
"id": "ast-a1b2c3d4",
"name": "HVAC Unit #12 (Lobby)",
"organizationId": "o-xxxx",
"assetTypeId": "at-HmlE8PRGLx",
"locationId": "l-sv09bQPYEK",
"status": "Normal",
"connectionStatus": false,
"isOnline": null
},
"previousAttributes": {
"connectionStatus": true
}
},
"livemode": true
}

Status fields explained

FieldTypeDescription
connectionStatusbooleantrue = device sent data within its keepAlive window. false = device is offline (no uplink within keepAlive).
statusstringOperational status: "Normal", "Warning", or "Critical". Set by alert rules, not connectivity.
isOnlineboolean \| nullLegacy field. Use connectionStatus instead.
Detecting online/offline transitions

Check previousAttributes.connectionStatus to determine the direction of change:

  • previousAttributes.connectionStatus: true → device just went offline
  • previousAttributes.connectionStatus: false → device just came back online
  • previousAttributes: {} (empty) → first time connectivity was recorded for this device

How connectivity detection works

Infodeck checks device connectivity every 2 minutes using this formula:

isConnected = lastUplinkTimestamp + (keepAlive × 1000) > now
  • keepAlive is configurable per asset (default: 86,400 seconds = 24 hours). For example, an indoor air quality sensor sending data every 30 minutes might use keepAlive: 1800.
  • When a device stops sending data and the keepAlive window expires, the next connectivity check writes connectionStatus: false and fires this webhook.
  • When the device resumes sending data, the next connectivity check writes connectionStatus: true and fires again.

Example: full online → offline → online cycle

Step 1 — Device is online, sending telemetry normally. No asset.status.changed fires (status unchanged).

Step 2 — Device goes offline (unplugged, out of range, or powered off). After the keepAlive window expires (e.g., 30 minutes for keepAlive: 1800), the next 2-minute connectivity check detects the device is offline:

{
"type": "asset.status.changed",
"data": {
"object": {
"connectionStatus": false
},
"previousAttributes": {
"connectionStatus": true
}
}
}

Step 3 — Device comes back online and sends telemetry. The next 2-minute connectivity check detects the device is online again:

{
"type": "asset.status.changed",
"data": {
"object": {
"connectionStatus": true
},
"previousAttributes": {
"connectionStatus": false
}
}
}
Maximum detection delay

The worst-case delay for detecting a status change is keepAlive + 2 minutes (keepAlive window expiry + next checker cycle). For a sensor with keepAlive: 1800 (30 min), expect the offline webhook within ~32 minutes of the last uplink. For reconnection, the webhook fires within 2 minutes of the first new uplink.


Work Order Events

workorder.created

Fired when a new work order is opened.

{
"id": "evt_wo_001",
"type": "workorder.created",
"organizationId": "o-xxxx",
"created": 1771920000,
"data": {
"object": {
"id": "wo-x1y2z3",
"title": "Fix leaking pipe in B2 washroom",
"description": "Water leaking from ceiling pipe near sink area",
"status": "Open",
"priority": "High",
"locationId": "loc-building-b",
"assigneeId": "usr-tech-01",
"reportedBy": "usr-fm-manager",
"slaResponseDeadline": 1771927200,
"slaResolutionDeadline": 1771963200,
"created": 1771920000,
"updated": 1771920000
}
},
"livemode": true
}

workorder.updated

Fired when a work order's details are modified (title, description, priority, assignee, etc.).

{
"id": "evt_wo_002",
"type": "workorder.updated",
"organizationId": "o-xxxx",
"created": 1771921000,
"data": {
"object": {
"id": "wo-x1y2z3",
"title": "Fix leaking pipe in B2 washroom",
"status": "Open",
"priority": "Critical",
"locationId": "loc-building-b",
"assigneeId": "usr-tech-02",
"created": 1771920000,
"updated": 1771921000
},
"previousAttributes": {
"priority": "High",
"assigneeId": "usr-tech-01"
}
},
"livemode": true
}

workorder.status.changed

Fired when a work order transitions to a new status.

{
"id": "evt_wo_003",
"type": "workorder.status.changed",
"organizationId": "o-xxxx",
"created": 1771925000,
"data": {
"object": {
"id": "wo-x1y2z3",
"title": "Fix leaking pipe in B2 washroom",
"status": "InProgress",
"priority": "Critical",
"created": 1771920000,
"updated": 1771925000
},
"previousAttributes": {
"status": "Open"
}
},
"livemode": true
}

workorder.completed

Fired when a work order is marked as completed.

{
"id": "evt_wo_004",
"type": "workorder.completed",
"organizationId": "o-xxxx",
"created": 1771940000,
"data": {
"object": {
"id": "wo-x1y2z3",
"title": "Fix leaking pipe in B2 washroom",
"status": "Completed",
"priority": "Critical",
"completedAt": 1771940000,
"completedBy": "usr-tech-02",
"created": 1771920000,
"updated": 1771940000
},
"previousAttributes": {
"status": "InProgress"
}
},
"livemode": true
}

Booking Events

booking.created

Fired when a new booking is made.

{
"id": "evt_bk_001",
"type": "booking.created",
"organizationId": "o-xxxx",
"created": 1771950000,
"data": {
"object": {
"id": "bk-m1n2o3",
"resourceId": "res-meeting-room-5a",
"resourceName": "Meeting Room 5A",
"locationId": "loc-building-a",
"bookedBy": "usr-john",
"startTime": 1771963200,
"endTime": 1771966800,
"status": "Confirmed",
"created": 1771950000
}
},
"livemode": true
}

booking.cancelled

Fired when a booking is cancelled.

{
"id": "evt_bk_002",
"type": "booking.cancelled",
"organizationId": "o-xxxx",
"created": 1771955000,
"data": {
"object": {
"id": "bk-m1n2o3",
"resourceId": "res-meeting-room-5a",
"status": "Cancelled",
"cancelledBy": "usr-john",
"cancelledAt": 1771955000,
"created": 1771950000
},
"previousAttributes": {
"status": "Confirmed"
}
},
"livemode": true
}

booking.checked_in

Fired when a guest checks in for their booking.

{
"id": "evt_bk_003",
"type": "booking.checked_in",
"organizationId": "o-xxxx",
"created": 1771963500,
"data": {
"object": {
"id": "bk-m1n2o3",
"resourceId": "res-meeting-room-5a",
"status": "CheckedIn",
"checkedInAt": 1771963500,
"created": 1771950000
},
"previousAttributes": {
"status": "Confirmed"
}
},
"livemode": true
}

Visitor Events

visitor.registered

Fired when a new visitor is pre-registered.

{
"id": "evt_vis_001",
"type": "visitor.registered",
"organizationId": "o-xxxx",
"created": 1771970000,
"data": {
"object": {
"id": "vis-p1q2r3",
"name": "Jane Smith",
"email": "jane.smith@example.com",
"company": "Acme Corp",
"hostId": "usr-john",
"locationId": "loc-building-a",
"expectedArrival": 1771984800,
"status": "Registered",
"created": 1771970000
}
},
"livemode": true
}

visitor.checked_in

Fired when a visitor checks in at the location.

{
"id": "evt_vis_002",
"type": "visitor.checked_in",
"organizationId": "o-xxxx",
"created": 1771984900,
"data": {
"object": {
"id": "vis-p1q2r3",
"name": "Jane Smith",
"status": "CheckedIn",
"checkedInAt": 1771984900,
"created": 1771970000
},
"previousAttributes": {
"status": "Registered"
}
},
"livemode": true
}

visitor.checked_out

Fired when a visitor checks out.

{
"id": "evt_vis_003",
"type": "visitor.checked_out",
"organizationId": "o-xxxx",
"created": 1771999200,
"data": {
"object": {
"id": "vis-p1q2r3",
"name": "Jane Smith",
"status": "CheckedOut",
"checkedOutAt": 1771999200,
"created": 1771970000
},
"previousAttributes": {
"status": "CheckedIn"
}
},
"livemode": true
}

SOR Events (Schedule of Rates)

sor.schedule.published

Fired when a SOR schedule is published and made active.

{
"id": "evt_sor_001",
"type": "sor.schedule.published",
"organizationId": "o-xxxx",
"created": 1772000000,
"data": {
"object": {
"id": "sor-sch-a1b2c3",
"name": "FY2026 Maintenance Rates",
"status": "Published",
"effectiveFrom": 1772000000,
"effectiveTo": 1803536000,
"itemCount": 245,
"publishedBy": "usr-fm-manager",
"publishedAt": 1772000000,
"created": 1771900000
},
"previousAttributes": {
"status": "Draft"
}
},
"livemode": true
}

sor.quotation.submitted

Fired when a SOR quotation is submitted for approval.

{
"id": "evt_sor_002",
"type": "sor.quotation.submitted",
"organizationId": "o-xxxx",
"created": 1772010000,
"data": {
"object": {
"id": "sor-qt-d4e5f6",
"scheduleId": "sor-sch-a1b2c3",
"workOrderId": "wo-x1y2z3",
"status": "Submitted",
"totalAmountCents": 1250000,
"lineItemCount": 8,
"submittedBy": "usr-contractor-01",
"submittedAt": 1772010000,
"created": 1772005000
},
"previousAttributes": {
"status": "Draft"
}
},
"livemode": true
}
info

Money values are always in integer cents. 1250000 = $12,500.00.

sor.quotation.approved

Fired when a SOR quotation is approved.

{
"id": "evt_sor_003",
"type": "sor.quotation.approved",
"organizationId": "o-xxxx",
"created": 1772020000,
"data": {
"object": {
"id": "sor-qt-d4e5f6",
"scheduleId": "sor-sch-a1b2c3",
"workOrderId": "wo-x1y2z3",
"status": "Approved",
"totalAmountCents": 1250000,
"approvedBy": "usr-fm-manager",
"approvedAt": 1772020000,
"created": 1772005000
},
"previousAttributes": {
"status": "Submitted"
}
},
"livemode": true
}

Test Events

webhook.test

Sent when you click "Send Test" in the dashboard or call the test endpoint. Use this to verify your endpoint is reachable and your signature verification works.

{
"id": "evt_test_xyz789",
"type": "webhook.test",
"organizationId": "o-xxxx",
"created": 1771911600,
"data": {
"object": {
"message": "This is a test webhook event from Infodeck"
}
},
"isTest": true,
"livemode": false
}
danger

Test events have livemode: false and isTest: true. Your handler should process them normally but avoid triggering side effects (e.g., do not create real records from test events).

Was this page helpful?