Webhooks
Your backend receives a signed event for every scan — the verified record of what happened, independent of anything the device says.
Setup
Set the webhook URL on your app in the portal (https only). Saving it issues your signing secret whsec_… — shown once. Rotating issues a new one and invalidates the old.
Events
| Event | When | Carries |
|---|---|---|
scan.completed | A member finished a scan | scan_id, host_user_ref, model version, status. Never a measurement. |
scan.failed | A scan ended in failure | Same shape, failed status. |
scan.results_available | Only with results delivery enabled | The member's result in data.result. See Results delivery. |
Verifying a delivery
Every delivery is signed. Compute HMAC-SHA256 of the raw request body with your whsec_… secret and compare it to the header — before parsing the JSON.
| Header | Value |
|---|---|
Visualize-Signature | Hex HMAC-SHA256 of the raw body, keyed with your secret |
Visualize-Event-Id | Stable per scan, across retries — dedupe on it |
Visualize-Event-Type | Same as type in the body |
import crypto from "node:crypto";
app.post("/hooks/visualize", express.raw({ type: "*/*" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.VISUALIZE_WEBHOOK_SECRET)
.update(req.body) // raw bytes, before any parsing
.digest("hex");
const given = req.get("Visualize-Signature") ?? "";
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body);
// handle event.type, dedupe on Visualize-Event-Id
res.status(200).end();
});Delivery semantics
At-least-once. Any 2xx acknowledges; anything else retries with exponential backoff from 30 seconds, capped at 6 hours, for 17 attempts (about 50 hours). Redirects are not followed. Because retries can duplicate, dedupe on Visualize-Event-Id.
scan.completed is deliberately measurement-free: it is the billing-grade record that a scan happened. Measurements reach your app from the SDK on the device — or server-side via the separate, consent-gated results delivery.