Webhooks

Webhooks push status changes to your own endpoints — a Slack relay, PagerDuty, a Lambda, whatever you run. TunnelHQ sends a clean event when a monitor goes down and another when it recovers.

On this page

Project webhooks

Set these up under Developers → Webhooks. They're edge-triggered: you get exactly one monitor.down when a monitor goes from up to down, and exactly one monitor.up when it recovers.

They don't repeat during an outage

A project webhook fires on the transition, not on every failed check. If a server is down for six hours, you get one monitor.down at the start and one monitor.up at recovery — never a stream of "still down" calls. That's exactly what HTTP receivers like Slack, Discord, and PagerDuty expect.

Events

Subscribe each endpoint to the events it cares about:

  • Statusmonitor.up, monitor.down, monitor.degraded (a retry signal — see below).
  • Incidentsincident.created, incident.resolved.
  • Lifecyclemonitor.created, monitor.updated, monitor.deleted, monitor.paused, monitor.resumed.

monitor.degraded is a retry signal, not a partial-failure signal

It fires when a monitor fails a check and starts retrying before being declared down — so a single-region monitor emits it on a brief network blip, not just a server that's failing in some regions and passing in others.

Every monitor.degraded is followed by a monitor.up when the monitor recovers, so a receiver never stays stuck in a degraded state. If you only want confirmed outages, subscribe to monitor.down and monitor.up and leave monitor.degraded off: a recovery that merely closes a brief retry is delivered only to endpoints that subscribed to the degraded event, so it won't reach you.

Not to be confused with the monitor status also called degraded, which is a different thing: that one means no region completed a check and at least one was definitely refused — credentials or a certificate rejected. The event is about retrying; the status is about a confirmed configuration problem.

Payload

Every delivery is a JSON POST with the same fixed envelope — event, timestamp, projectId, and an event-specific data object:

monitor.down
{
  "event": "monitor.down",
  "timestamp": "2026-06-14T08:05:10.000Z",
  "projectId": 42,
  "data": {
    "monitor_id": "srv_916",
    "monitor_name": "Sydney-I",
    "protocol": "openconnect",
    "host": "95.111.222.15",
    "port": 443,
    "time": "2026-06-14T08:05:10.000Z",
    "latency_ms": null,
    "message": "Tunnel unreachable"
  }
}

Verifying deliveries

Give an endpoint a secret and every delivery is signed: the X-TunnelHQ-Signature header carries sha256=<HMAC-SHA256 of the raw body>. Recompute the HMAC with your secret and compare before trusting the payload — rejecting anything whose header is missing or malformed, which an attacker controls and a legitimate delivery never is. Deliveries also include X-TunnelHQ-Event and X-TunnelHQ-Endpoint-Id headers for routing.

verify.js
const crypto = require("crypto");

function verify(rawBody, signatureHeader, secret) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = Buffer.from(String(signatureHeader ?? ""), "utf8");
  const digest = Buffer.from(expected, "utf8");
  // timingSafeEqual throws unless both buffers are the same length, so a
  // missing or truncated header has to be rejected before it gets there.
  // Comparing lengths first leaks nothing: the digest length is fixed.
  return received.length === digest.length &&
    crypto.timingSafeEqual(received, digest);
}

Delivery log

Every outbound call is recorded with its timestamp and response, so you can see exactly what was sent and when. If you ever wonder whether a webhook fired, the delivery log is the source of truth — not the dashboard's "still down" heartbeat rows, which are status updates, not webhook calls.

Notification webhooks (legacy)

There's a second, older path under Settings → Notifications → Add Webhook, inherited from the monitoring engine TunnelHQ is built on. It also fires on the transition, but it has an optional resend interval — set it above zero and it re-sends every Nth failed check during an outage. It's off by default, which matches the project-webhook behavior above. Reach for it only if you specifically want repeat reminders while something is down.

Which should I use?

For new integrations, use project webhooks — clean, edge-triggered events that modern receivers handle well. Use the notification-provider webhook only when you need the repeat-while-down behavior.