Connect your systems

Webhooks

What Started sends a signed JSON message to your URL when something happens, so your systems hear about a stop without asking. Webhooks are included in Pro and Plant; your plan sets how many endpoints you can add. An owner or admin adds them under Settings → Data and API. To ask for data instead, use the read API.

Events

Type When
machine.status_changed A ping or an operator changes a machine's status
alert.opened A machine loses its signal, goes down, stops producing, or a metric leaves its normal range
alert.resolved That condition ends
parts.logged Someone logs good or bad parts by hand

Request

POST /your/path
Content-Type: application/json
User-Agent: Heartbeat-Webhooks/1
Heartbeat-Event: alert.opened
Heartbeat-Delivery: 8f14e45f-ceea-4e7a-9b1c-1c2d3e4f5a6b
Heartbeat-Signature: t=1791712800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
  "id": "8f14e45f-ceea-4e7a-9b1c-1c2d3e4f5a6b",
  "type": "alert.opened",
  "createdAt": "2026-10-11T07:40:02.511Z",
  "data": {
    "alertId": 311,
    "kind": "down",
    "machineId": "…",
    "machineName": "Press 4",
    "metricKey": null,
    "metricValue": null,
    "openedAt": "2026-10-11T07:40:02+00:00",
    "resolvedAt": null
  }
}

Alert kind is no_signal, down, not_producing or metric_range. For metric_range, metricKey names the metric and metricValue is the value that opened the alert.

machine.status_changed data has eventId, machineId, machineName, status, reasonCode, reasonLabel, note, source, operatorLabel and occurredAt.

Verify the signature

The signature is an HMAC-SHA256, keyed with the endpoint's signing secret (hbw_…, shown once when you add it), over <t>.<raw body>. Reject messages whose t is more than 5 minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}

Retries

Any 2xx response is a success. Anything else, a timeout (10 seconds) or a redirect counts as a failure and is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. A retry has the same id, so de-duplicate on it. Deliveries can arrive out of order; use occurredAt or openedAt for ordering.

An endpoint that fails 50 deliveries in a row is paused. Resume it under Settings → Data and API once it is fixed.

Allowed URLs

https only, no credentials in the URL, and the host must resolve to a public address.