Skip to main content

Webhooks

Webhooks are the push side of the API. Instead of your application asking ioX-Pulse for data on a timer, ioX-Pulse sends each event to a URL you choose, as it happens. This is the recommended way to react to new readings, alarms, and devices going on or offline in real time.

note

Where to find it: Sidebar, then Account settings, then the Integrations tab, then the Webhooks sub-tab.

How webhooks work

  1. You register a webhook: an HTTPS URL, the events you care about, and an optional device filter.
  2. When a matching event happens, ioX-Pulse sends an HTTP POST to your URL with a JSON body describing the event.
  3. Your endpoint verifies the signature, does its work, and replies with a 2xx status code.
  4. If your endpoint is unreachable or returns a non-2xx status, ioX-Pulse retries on a backoff schedule. See Delivery and retries.

Each webhook has its own signing secret, shown when you create it, so you can confirm that a request genuinely came from ioX-Pulse.

Permission required: Partner Admin, or Sub-account Admin for a webhook scoped to a sub-account.

Creating a webhook

  1. Open Account settings and select the Integrations tab, then Webhooks.
  2. Click Create webhook.
  3. Enter a recognizable name (for example, Ops Slack relay).
  4. Enter the payload URL. It must be https://. See Security.
  5. Choose the events to subscribe to. See the Event catalog.
  6. Optionally set a device filter so only some devices trigger delivery. See Filtering which devices fire.
  7. Click Create.

The signing secret is shown after creation, in the form whsec_.... Store it somewhere safe in your receiver. You will need it to verify signatures. Unlike an API key, the secret stays revealable on the webhook detail view, because you need it to keep verifying deliveries.

Event catalog

EventFires whendata payload
telemetry.readingA decoded uplink is ingested for a device.The decoded field values, keyed by field identifier.
device.onlineA device that was offline reports again.Empty.
device.offlineA device goes silent past its offline threshold.lastUplinkAt, offlineThresholdMinutes.
alarm.triggeredA rule enters an alert state, or changes level.ruleId, ruleName, level, previousLevel.
alarm.clearedAn alert state clears.ruleId, level, reason.

Notes:

  • telemetry.reading fires only when a device has a decoder that produced values. A raw frame with nothing decoded does not fire it.
  • device.offline is detected by a periodic sweep, so it arrives shortly after the threshold elapses, not to the second. The offline threshold is set per device on its Configuration tab.
  • alarm.triggered does not repeat for a re-fire at the same level. It fires for a new alarm or a level change (for example, warning to alert).
  • alarm.cleared's reason is one of ack, rule_disabled, rule_deleted, expiry, device_moved (the device was unassigned, transferred, or claimed into another account), profile_changed (the device was moved off the profile whose workflow raised the alarm), device_deleted, or sub_closed (the account holding the device was closed).
  • You may also receive workflow.fired deliveries. Those come from a workflow's Webhook action node, not from a webhook integration on this page: they go to the URL on the node, are signed with that node's own secret, and carry a notification-shaped payload instead of the envelope below. Device workflows carry a device object in that payload; gateway workflows carry a gateway object (id, name, gatewayEui) instead. The signature scheme is identical, so the verification code on this page covers both. See Workflows.

Payload shape

Every delivery has the same envelope. Only the data object differs per event.

{
"event": "telemetry.reading",
"occurredAt": "2026-05-30T14:57:10.000Z",
"partnerId": "445e...cfc",
"subAccountId": "775e...cfc",
"device": {
"id": "d4e9...cfc",
"devEui": "5a4dba5e00000001",
"name": "Cold store probe",
"profileId": "9f1c...a21"
},
"site": {
"id": "a1b2...cfc",
"name": "Main warehouse"
},
"data": { "valve_state": "Closed", "is_poll": true, "temp_c": 21.8 },
"signalQuality": 48,
"retained": { "valve_state": "2026-05-30T11:20:00.000Z" },
"derived": { "temperature_f": 71.2 }
}
  • event: the event type, from the catalog above.
  • occurredAt: when the event actually happened, as an ISO-8601 timestamp. For telemetry.reading and device.online it is the uplink time. For device.offline it is the moment the device went silent (its last uplink plus its threshold). For alarm events it is when the rule state changed.
  • partnerId and subAccountId: the tenancy the event belongs to. subAccountId is null for a partner-level device.
  • device: the device the event is about, with its id, devEui, name, and profileId.
  • site: where the device was when the event happened, as { id, name }, or null if the device has no site. Captured at the moment the event fires, so it reflects the device's location at event time even if delivery is delayed. Present only when your tier has multi-site.
  • data: the per-event payload from the table above.
  • signalQuality: the device's Signal Quality % (0-100) at event time, derived from RSSI and SNR. Present on telemetry.reading (this frame's value), device.online (the recovering uplink's value), and device.offline (the last-known value before it went silent). Absent when no signal is known.
  • retained: present only on telemetry.reading, and only when one or more values were carried forward from an earlier uplink (the Retain last value option). It maps each carried key in data to the ISO-8601 time that value was last actually reported. A key in data but not in retained was reported fresh on this uplink. When nothing was carried, the field is absent, so existing payloads are unchanged until a profile turns retention on.
  • derived: present only on telemetry.reading, and only when the profile has formula or mapping fields or the device has a calibrated field. It carries this reading's derived values keyed by field identifier: formula and mapping outputs, and the calibrated value of a calibrated field. data stays the decoder's output. A derived value is computed once when the reading arrives and stored with it, so what you receive is exactly what dashboards, history and exports show for that reading; a later change to a formula, mapping or calibration applies to new readings only.

The same envelope is sent to every webhook subscribed to the event, so two endpoints receive identical bodies.

Verifying the signature

Every delivery carries a signature header so you can confirm it came from ioX-Pulse and was not tampered with. Always verify before trusting a payload.

Each request includes these headers:

HeaderMeaning
X-Pulse-EventThe event type, for quick routing.
X-Pulse-DeliveryA unique id for this delivery attempt.
X-Pulse-SignatureThe signature, in the form t=<unix>,v1=<hex>.

The signature is an HMAC-SHA256 of the timestamp and the raw request body, joined with a dot, keyed by your webhook's signing secret:

v1 = HMAC_SHA256(secret, "<t>.<raw-body>")

To verify a request:

  1. Read t and v1 from the X-Pulse-Signature header.
  2. Compute your own HMAC over "<t>.<raw-body>" using the signing secret. Use the raw body bytes, before any JSON parsing.
  3. Compare your result to v1 with a constant-time comparison.
  4. Reject the request if the values differ, or if t is more than five minutes from now. The timestamp check limits replay of a captured request.
import crypto from 'node:crypto'

// `rawBody` is the exact bytes received, before JSON.parse.
function verifyPulseSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((p) => p.trim().split('=')),
)
const t = Number(parts.t)
const v1 = parts.v1
if (!t || !v1) return false
if (Math.abs(Date.now() / 1000 - t) > 300) return false // 5-minute tolerance

const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex')

const a = Buffer.from(expected, 'hex')
const b = Buffer.from(v1, 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Delivery and retries

Deliveries are sent by a background worker, so they arrive within about a minute of the event, not necessarily instantly. Your endpoint should respond quickly (within ten seconds) with any 2xx status. Do the slow work afterward, not while ioX-Pulse waits.

If a delivery fails (a non-2xx status, a timeout, or an unreachable host), it is retried up to four times total on this schedule:

AttemptSent after
1immediately
230 seconds
35 minutes
430 minutes

After the fourth failed attempt the delivery is marked failed and not retried again.

caution

A dead endpoint is switched off. After many consecutive failures across deliveries, the webhook is automatically disabled so a broken endpoint stops generating traffic. Re-enable it from the Integrations tab once your endpoint is healthy again. Re-enabling clears the failure count.

Make your endpoint idempotent. A retry can deliver the same event twice, and the X-Pulse-Delivery id lets you recognize and skip a duplicate.

Delivery log

Each webhook has a delivery log on its detail view, showing recent attempts with their status, attempt count, and any error. Use it to confirm deliveries are landing and to diagnose failures.

Completed entries (sent or failed) are kept for 14 days and then removed automatically. Deliveries still pending or in flight are never removed, regardless of age.

Pausing a webhook

Each row on the Integrations tab has a switch that pauses or resumes the webhook without deleting it. While a webhook is paused, deliveries are skipped, not queued: events that happen while it is paused are never delivered, and delivery resumes from new events when you switch it back on. The same switch also appears in the webhook's edit drawer.

Turning the switch on also revives a webhook that was automatically disabled after repeated failures; the failure count is cleared.

Filtering which devices fire

By default a webhook fires for every device in its scope. You can narrow it with a device filter when you create or edit the webhook:

  • All devices (the default).
  • By profile: only devices on the chosen device profiles.
  • By device: only the chosen devices.
  • By tag: only devices carrying any of the chosen tags.
  • By site: only devices at the chosen sites or any site nested under them, based on where the device is when the event fires. Available when your tier has multi-site. See Sites.

Scope still applies on top of the filter. A partner-level webhook can fire for any device in your partner account, including those assigned to sub-accounts. A sub-account webhook fires only for that sub-account's devices.

Security

  • HTTPS only. Payload URLs must be https://. Plain http:// is rejected.
  • No internal targets. ioX-Pulse refuses URLs that resolve to private, loopback, link-local, or cloud-metadata addresses, both when you save a webhook and again at delivery time. This protects against requests being aimed at internal infrastructure.
  • Verify every payload. Treat the signing secret like a password, and check the signature on every request before acting on it. See Verifying the signature.

Testing

The webhook detail view has a Send test button. It delivers a sample payload to your URL right away and reports the result, so you can confirm your endpoint is reachable and your signature check passes before any real event fires.

If you do not have an endpoint yet, a public request inspector that returns 2xx is a quick way to see a real delivery and its headers.

Next steps