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.