Ionhour Docs
Developer Platform

Webhooks

Subscribe your own app to Ionhour events — configuring delivery, the signed envelope format, verifying HMAC signatures, rotating secrets, and retry behavior.

Subscribed events are POSTed to your app's delivery endpoint — one delivery per installation, signed with your webhook signing secret.

Configuring delivery

On the app's Webhooks tab set an https endpoint URL and tick the events you want:

EventFires whenGated by
check.upA check recoverschecks:read
check.downA check goes downchecks:read
check.lateA check misses its schedulechecks:read
check.pausedA check is paused — manually or by a deployment window (data.trigger says which)checks:read
check.resumedA paused check resumeschecks:read
job.upA heartbeat job recoversjobs:read
job.downA heartbeat job goes downjobs:read
job.lateA heartbeat job misses its schedulejobs:read
incident.createdAn incident opensincidents:read
incident.acknowledgedAn incident is acknowledgedincidents:read
incident.resolvedAn incident resolvesincidents:read

Check events carry data.checkId; job events carry data.jobId — checks (outbound probes) and jobs (inbound heartbeats) are separate domains with separate scopes. More event families (deployments, maintenance windows, dependencies, status pages) are planned and will be added to this list additively.

Scope gating. An event is delivered to an installation only if that installation granted the event's backing read scope. Subscribing without the scope isn't an error — the event is simply never delivered (the console warns you). Note that gating is per installation, not per app: if your app requests checks:read but a particular workspace's installation granted fewer scopes, that installation receives nothing for the gated events. A webhook payload carries the same information a read would, so this keeps event delivery inside the consent your users granted.

The delivery envelope

{
  "id": "evt_…",
  "type": "incident.created",
  "createdAt": "2026-07-19T20:15:00.000Z",
  "workspaceId": "…",
  "installationId": "…",
  "data": { }
}

Verifying signatures

Every delivery carries two headers:

  • X-Ionhour-Timestamp — Unix timestamp of the send
  • X-Ionhour-Signaturev1=<hex HMAC-SHA256> computed over <timestamp>.<raw body> with your ionh_whsec_… secret

Verify before trusting a payload:

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

function verify(secret, timestamp, rawBody, signatureHeader) {
  const skewSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (skewSeconds > 300) return false; // reject > 5 min skew (replay guard)

  const expected = `v1=${createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')}`;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Compute the HMAC over the raw request body, before any JSON parsing — re-serialized JSON will not match.

Secret rotation

Rotate the signing secret from the Webhooks tab. The new secret takes over immediately, and the previous secret stays valid for 24 hours — verify against both during that window and you can roll your fleet without dropping deliveries.

Retries and delivery health

Failed deliveries are retried with exponential backoff and logged. Sustained failure surfaces as delivery-health on the workspace's Installed Apps page and can eventually auto-pause the subscription. Deliveries stop immediately when an app is suspended or rejected, or when an installation is revoked.