Skip to content
Firewatch
Get started
Site

Outbound alert webhooks

Organization administrators can configure up to ten outbound endpoints under Organization → Integrations. Firewatch sends an incident.notification.requested event for each escalation notification. The payload contains structured incident, service, recipient, and escalation data, plus the organization’s rendered webhook message template.

Every request includes:

Header Meaning
X-Firewatch-Event incident.notification.requested
X-Firewatch-Delivery Stable UUID for this notification delivery and its retries
X-Firewatch-Timestamp Unix timestamp in seconds
X-Firewatch-Signature v1= followed by a lowercase HMAC-SHA256 hex digest

The signature base is the exact UTF-8 string:

<timestamp>.<delivery UUID>.<raw request body>

Compute HMAC-SHA256 with the endpoint’s fwhsec_... signing secret and compare the complete v1=<hex> value in constant time. Read and verify the raw body before JSON parsing; re-serializing JSON changes the signed bytes.

Receivers should also:

  1. reject timestamps more than five minutes away from their current time;
  2. persist processed X-Firewatch-Delivery values and ignore duplicates;
  3. return a 2xx response only after durably accepting the event;
  4. keep the signing secret in a secret manager and rotate it from the UI if it may have been exposed.

The signing secret is displayed only when an endpoint is created or rotated. Firewatch stores only its encrypted form. Rotation invalidates the previous secret immediately.

import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyFirewatchWebhook({
secret,
rawBody,
deliveryId,
timestamp,
signature,
now = Date.now(),
}) {
const timestampNumber = Number(timestamp);
if (!Number.isSafeInteger(timestampNumber)) return false;
if (Math.abs(now / 1000 - timestampNumber) > 300) return false;
const base = `${timestamp}.${deliveryId}.${rawBody}`;
const expected = `v1=${createHmac("sha256", secret)
.update(base, "utf8")
.digest("hex")}`;
const expectedBytes = Buffer.from(expected, "ascii");
const actualBytes = Buffer.from(signature, "ascii");
return (
expectedBytes.length === actualBytes.length &&
timingSafeEqual(expectedBytes, actualBytes)
);
}

Redirects are not followed. HTTPS is required, and localhost, private, link-local, and other unsafe network destinations are blocked by default to reduce SSRF risk. A self-hoster can deliberately opt into an internal receiver with FIREWATCH_WEBHOOK_ALLOW_PRIVATE_NETWORKS=true; plain HTTP additionally requires FIREWATCH_WEBHOOK_ALLOW_HTTP=true. Keep both disabled for an internet-facing deployment unless there is a reviewed operational need.