◉
24observe
DOCUMENTATION • WEBHOOKS • 5 MIN READ

Verify the webhook came from us.

When we POST to a URL you configured, we sign it. Your receiver verifies the signature to be certain the request originated here and wasn't tampered with in transit. Same signing scheme covers both kinds of outbound webhook below.

Start free → Quickstart

TL;DR

When we POST to a URL you configured, we sign it. Your receiver verifies the signature to be certain the request originated here and wasn't tampered with in transit. Same signing scheme covers both kinds of outbound webhook below.

Docs · webhooks

Verify the webhook came from us.

When we POST to a URL you configured, we sign it. Your receiver verifies the signature to be certain the request originated here and wasn't tampered with in transit. Same signing scheme covers both kinds of outbound webhook below.

Two kinds of outbound webhook

We ship two distinct webhook surfaces. They use the same signing scheme, so the verification code below works for both — but they're configured in different places.

  • Per-monitor alert webhook — one URL stored on the monitor row. Fires when that specific monitor goes down. Payload is flat: {monitor, url, status, error, timestamp}. Configure via the monitor edit form's "Webhook URL" field, or PATCH /api/v1/monitors/:id {"alertWebhookUrl": "..."}.
  • Event subscriptions — register a URL for one or more event types (incident.opened, incident.acknowledged, incident.resolved, monitor.status_changed, log_alert.fired). Payload is a structured envelope: {id, type, created, data: {...}}. Configure under Settings → Event webhooks, or POST /api/v1/webhook-subscriptions. Full walkthrough at /docs/webhook-subscriptions/ — covers CRUD, retry behaviour, auto-disable threshold, and per-subscription delivery log.

Both ship from User-Agent: 24observe-webhooks/1.0. Event subscriptions also send X-24Observe-Event and X-24Observe-Delivery-Id headers so your handler can branch on event type and dedupe retries.

Headers we send

X-24Observe-Timestamp: 1777200000
X-24Observe-Signature: t=1777200000,v1=<64-char-hex>

Verification recipe

  1. Read the t= value from the signature header.
  2. Reject if it's older than your tolerance window (we recommend 5 minutes — replay protection).
  3. Compute HMAC-SHA256(secret, "{t}.{rawBody}").hex()
  4. Compare with the v1= value using a constant-time comparison.

The secret is the per-organization webhook signing secret you can read and rotate from Settings → Webhook signing secret in your dashboard.

Node.js (Express)

import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';

const SECRET = process.env.OBSERVE24_WEBHOOK_SECRET; // from your dashboard
const app = express();

// IMPORTANT: use the raw body, not parsed JSON.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sigHeader = req.header('x-24observe-signature') || '';
  const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
  const t = Number(parts.t);
  const v1 = parts.v1;

  // Replay defense: reject if timestamp is more than 5 minutes off.
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);

  const expected = createHmac('sha256', SECRET)
    .update(`${t}.${req.body.toString('utf8')}`)
    .digest();
  const actual = Buffer.from(v1, 'hex');
  if (actual.length !== expected.length) return res.sendStatus(400);
  if (!timingSafeEqual(expected, actual)) return res.sendStatus(401);

  // Trusted! Parse and process.
  const payload = JSON.parse(req.body.toString('utf8'));
  console.log('alert:', payload);
  res.sendStatus(204);
});

Python (FastAPI)

import hmac, hashlib, time, os
from fastapi import FastAPI, Request, HTTPException

SECRET = os.environ['OBSERVE24_WEBHOOK_SECRET'].encode()
app = FastAPI()

@app.post('/webhook')
async def webhook(request: Request):
    raw = await request.body()
    sig_header = request.headers.get('x-24observe-signature', '')
    parts = dict(p.split('=', 1) for p in sig_header.split(','))
    t = int(parts.get('t', 0))
    v1 = parts.get('v1', '')

    if abs(time.time() - t) > 300:
        raise HTTPException(400, 'timestamp out of tolerance')

    expected = hmac.new(SECRET, f'{t}.{raw.decode()}'.encode(), hashlib.sha256).digest()
    actual = bytes.fromhex(v1)
    if not hmac.compare_digest(expected, actual):
        raise HTTPException(401, 'bad signature')

    return {'ok': True}

Go

package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "fmt"
  "io"
  "net/http"
  "os"
  "strconv"
  "strings"
  "time"
)

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

func handler(w http.ResponseWriter, r *http.Request) {
  body, _ := io.ReadAll(r.Body)
  sigHeader := r.Header.Get("X-24Observe-Signature")
  parts := map[string]string{}
  for _, p := range strings.Split(sigHeader, ",") {
    kv := strings.SplitN(p, "=", 2)
    if len(kv) == 2 { parts[kv[0]] = kv[1] }
  }
  t, _ := strconv.ParseInt(parts["t"], 10, 64)
  if abs(time.Now().Unix() - t) > 300 { http.Error(w, "stale", 400); return }

  mac := hmac.New(sha256.New, secret)
  fmt.Fprintf(mac, "%d.%s", t, body)
  expected := mac.Sum(nil)
  actual, _ := hex.DecodeString(parts["v1"])
  if !hmac.Equal(expected, actual) { http.Error(w, "bad sig", 401); return }
  w.WriteHeader(204)
}

func abs(x int64) int64 { if x < 0 { return -x }; return x }

Common mistakes

  • Parsing the body before verifying. JSON.parse may reorder keys; you must hash the exact bytes we sent.
  • Non-constant-time comparison. Use timingSafeEqual / hmac.compare_digest / hmac.Equal — never ==.
  • Missing replay defense. Without the 5-minute timestamp check, a captured request can be replayed indefinitely.
  • Confusing the signing secret with your PAT. The signing secret is per-org (Settings → Webhook signing secret); your PAT is per-user (Settings → API tokens). Different things.

Want to test before going live?

Open any monitor → "Send test alert to all configured channels" — we POST a sample payload to your webhook URL with a real signature. Verify locally; you've got the recipe right when your endpoint returns 2xx.

Ready to build with 24Observe?

Explore live telemetry, zero-friction integration, and automated SOC investigation.

+ Get Free Trial / Demo