We use Google Analytics and a Meta advertising pixel on our public site only — never on the clinical application. You can opt out at any time via “Cookie settings” in the footer. See our privacy policy.

CardioBridge API

Webhooks

Instead of polling, configure a webhook URL and we'll POST a JSON event to it whenever a job reaches a terminal state. This is the recommended way to receive results.

Request headers

Every delivery includes these headers:

  • X-CardioBridge-Event — reported or returned.
  • X-CardioBridge-Signature — t=<unix_seconds>,v1=<hex>.

Verifying the signature

The signature is an HMAC-SHA256 over the string <t>.<raw_request_body> using your organisation's webhook signing secret (starts with whsec_, found in the app under Integrations). Compute the HMAC over the raw body bytes — not a re-serialised object — and compare with a constant-time function.

Prevent replay
Reject the event if t is missing, or if the absolute difference between t and the current unix time in whole seconds is greater than 300 (5 minutes). Use a constant-time compare ( timingSafeEqual / compare_digest) to avoid timing attacks.

Idempotent handling

Delivery is best-effort and an event may arrive more than once. Make your handler idempotent by de-duplicating on reference + event. Return 200 to acknowledge receipt.

Delivery headers
X-CardioBridge-Event: reported
X-CardioBridge-Signature: t=1719662400,v1=4f8a1c...e2b9
Verify the signature
const crypto = require("crypto");

// secret is your org's webhook signing secret (starts with "whsec_").
function verify(rawBody, sigHeader, secret) {
  const p = Object.fromEntries(sigHeader.split(",").map((kv) => kv.split("=")));
  // Reject if t is missing or more than 300 seconds (5 minutes) from now, either way.
  if (!p.t || Math.abs(Math.floor(Date.now() / 1000) - Number(p.t)) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${p.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}

// Express example — note express.raw() to keep the body bytes intact.
app.post("/cardiobridge/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verify(req.body.toString(), req.header("X-CardioBridge-Signature"), process.env.WHSEC);
  if (!ok) return res.status(400).send("invalid signature");
  const event = JSON.parse(req.body.toString());
  // Dedupe on (reference + event) — delivery is best-effort.
  res.sendStatus(200);
});