Skip to content

11.5Code examples

Each example reads the raw body, checks the signature with a constant-time comparison, and replies at once. Store the secret in the environment variable OBSRVLY_WEBHOOK_SECRET.

Node.js (Express)#

import crypto from "node:crypto";
import express from "express";

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

// express.raw keeps the exact bytes. Do not use express.json here.
app.post(
  "/alerts",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const header = req.get("X-Obsrvly-Signature") || "";
    if (!header.startsWith("sha256=")) return res.sendStatus(401);
    const sent = Buffer.from(header.slice(7), "hex");
    const expected = crypto
      .createHmac("sha256", SECRET)
      .update(req.body)
      .digest();
    if (
      sent.length !== expected.length ||
      !crypto.timingSafeEqual(sent, expected)
    ) {
      return res.sendStatus(401);
    }
    res.sendStatus(204); // reply first

    const alert = JSON.parse(req.body.toString("utf8"));
    console.log(alert.event, alert.alert_id, alert.monitor_name);
  },
);

app.listen(3000);

Python (Flask)#

import hashlib
import hmac
import os

from flask import Flask, abort, request

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


@app.post("/alerts")
def alerts():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    header = request.headers.get("X-Obsrvly-Signature", "")
    digest = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    expected = "sha256=" + digest
    if not hmac.compare_digest(header.encode(), expected.encode()):
        abort(401)
    alert = request.get_json()
    print(alert["event"], alert["alert_id"], alert.get("monitor_name"))
    return "", 204

Go#

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "log"
    "net/http"
    "os"
    "strings"
)

var secret = []byte(os.Getenv("OBSRVLY_WEBHOOK_SECRET"))

func alerts(w http.ResponseWriter, r *http.Request) {
    raw, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "bad body", http.StatusBadRequest)
        return
    }
    header := r.Header.Get("X-Obsrvly-Signature")
    if !strings.HasPrefix(header, "sha256=") {
        http.Error(w, "no signature", http.StatusUnauthorized)
        return
    }
    sent, err := hex.DecodeString(strings.TrimPrefix(header, "sha256="))
    mac := hmac.New(sha256.New, secret)
    mac.Write(raw)
    if err != nil || !hmac.Equal(sent, mac.Sum(nil)) {
        http.Error(w, "bad signature", http.StatusUnauthorized)
        return
    }
    var alert map[string]any
    if err := json.Unmarshal(raw, &alert); err != nil {
        http.Error(w, "bad json", http.StatusBadRequest)
        return
    }
    w.WriteHeader(http.StatusNoContent)
    log.Println(alert["event"], alert["alert_id"], alert["monitor_name"])
}

func main() {
    http.HandleFunc("/alerts", alerts)
    log.Fatal(http.ListenAndServe(":3000", nil))
}

Common mistakes#

  • Parsing the JSON first. Parsing and re-encoding the body changes its bytes, so the signature no longer matches. Check the raw body.
  • Comparing with ==. Use a constant-time comparison, as in the examples.
  • Using the wrong secret. Use the channel's HMAC signing secret, not an API key.
  • Leaving out sha256=. The header value is sha256= followed by the hex signature.
  • Extra spaces in the secret. obsrvly uses the secret exactly as typed. Make sure your copy matches.

Last updated