Browse the docs

Verify signatures

Prove a delivery came from Status Bee before you act on it.

On this page
  1. Node.js
  2. Python
  3. Check a saved delivery by hand
  4. Rotating the secret
  5. Status page subscribers who chose the webhook channel

Every delivery carries an X-Signature header: the string sha256= followed by the hex-encoded HMAC-SHA256 of the request body, keyed with the secret you were shown when the webhook was created. Recompute it over the raw bytes you received and compare with a constant-time comparison. If they differ, discard the request.

Use the raw body
Compute the HMAC over the body exactly as it arrived. Parsing the JSON and re-serialising it will change whitespace and key order, and the signature will not match. Read the raw body first.

Node.js

server.js
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.STATUSBEE_WEBHOOK_SECRET;

// Keep the raw body: the signature is computed over the exact bytes we sent.
app.post("/statusbee", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const given = req.get("X-Signature") || "";

  if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
    return res.status(401).send("bad signature");
  }

  const event = JSON.parse(req.body);
  // Acknowledge first, then do the work: a slow handler is retried.
  res.status(204).end();

  if (event.type === "monitor.down") {
    console.log(`${event.data.name} is down: ${event.data.error}`);
  }
});

app.listen(3000);

Python

app.py
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["STATUSBEE_WEBHOOK_SECRET"].encode()


@app.post("/statusbee")
def statusbee():
    raw = request.get_data()  # the exact bytes, before any parsing
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    given = request.headers.get("X-Signature", "")
    if not hmac.compare_digest(given, expected):
        abort(401)

    event = json.loads(raw)
    if event["type"] == "incident.resolved":
        print("resolved:", event["data"]["title"])
    return "", 204

Check a saved delivery by hand

Save the body of a delivery to a file (without trailing newline) and run:

Shell
# Recompute the signature for a saved delivery body
printf '%s' "$(cat delivery.json)" \
  | openssl dgst -sha256 -hmac "$STATUSBEE_WEBHOOK_SECRET"

The output should equal the X-Signature header without its sha256= prefix.

Rotating the secret

Secrets cannot be read back or rotated in place. To rotate, create a second webhook with the same URL and events, move your verifier to the new secret, then delete the old webhook. Both will deliver during the overlap, so the handler must accept either secret for a short while.

Status page subscribers who chose the webhook channel

A subscriber can subscribe to a public status page with a webhook URL instead of an email address. Those deliveries use the same envelope and headers, but the signing secret is that subscriber's unsubscribe token, not a workspace secret. They only receive incident and maintenance events.