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.
Request headers
Section titled “Request headers”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:
- reject timestamps more than five minutes away from their current time;
- persist processed
X-Firewatch-Deliveryvalues and ignore duplicates; - return a
2xxresponse only after durably accepting the event; - 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.
Node.js verification example
Section titled “Node.js verification example”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) );}Destination safety
Section titled “Destination safety”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.