Skip to main content

Overview

Immutable signs all outgoing webhook payloads (from alert rules and log streams) with HMAC-SHA256. The signature is included in the X-Immutable-Signature header.

Signature Format

X-Immutable-Signature: sha256={hex_encoded_hmac}
The HMAC is computed using your webhook’s signing secret as the key and the raw JSON request body as the message.

Verification

Always use constant-time comparison when verifying signatures to prevent timing attacks. Never use === or == for signature comparison.
import { createHmac, timingSafeEqual } from "crypto";

function verifyImmutableSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string
): boolean {
  const computed = "sha256=" + createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  // Constant-time comparison to prevent timing attacks
  if (computed.length !== signatureHeader.length) {
    return false;
  }

  return timingSafeEqual(
    Buffer.from(computed),
    Buffer.from(signatureHeader)
  );
}

// Express.js example
app.post("/webhook/immutable", (req, res) => {
  const rawBody = req.body; // must be raw string, not parsed JSON
  const signature = req.headers["x-immutable-signature"];

  if (!verifyImmutableSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const payload = JSON.parse(rawBody);
  // Process the verified payload
  console.log(payload.event_type); // "alert.triggered" or "event.created"

  res.status(200).send("OK");
});

Payload Types

Alert Webhooks

Sent when an alert rule triggers:
{
  "event_type": "alert.triggered",
  "alert_id": "alt_4f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
  "rule_name": "Suspicious Login",
  "rule_type": "new_country",
  "reason": "Actor user_2hG9kLm logged in from DE for the first time",
  "event": {
    "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "action": "user.login",
    "actor_id": "user_2hG9kLm",
    "ip_country": "DE"
  },
  "workspace_id": "ws_1a2b3c4d",
  "triggered_at": "2026-03-26T08:45:12.000000Z"
}

Log Stream Webhooks

Sent when a new event is ingested (via log streams):
{
  "event_type": "event.created",
  "data": {
    "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "actor_id": "user_2hG9kLm",
    "action": "document.created",
    "resource_id": "doc_8nXpQr3"
  },
  "workspace_id": "ws_1a2b3c4d",
  "timestamp": "2026-03-26T10:15:00.000000Z"
}

Important Notes

  • Raw body required — You must verify against the raw request body string, not a serialized/parsed version. Re-serializing parsed JSON may change key ordering or whitespace, invalidating the signature.
  • Signing secret — Set your signing secret when creating or editing a webhook destination in the dashboard. Store it securely as an environment variable.
  • Failed verification — Return a non-2xx status code if verification fails. Do not process unverified payloads.