Webhook events

Payload shape, signature verification and retry behaviour for alert webhooks.

When an alert fires, Pulse posts JSON to every webhook URL configured on the account. The request is signed so the receiver can prove it came from us.

Request

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: PulsePerimeter-Webhook/1.0
X-Pulse-Event-ID: 3f2b9c14-...
X-Pulse-Timestamp: 1791158400
X-Pulse-Signature: sha256=8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4
{
  "event_id": "3f2b9c14-...",
  "condition": "monitor.down",
  "severity": "critical",
  "title": "nightly-backup is down",
  "message": "No ping received for 360s (interval 300s + grace 60s).",
  "context": { "monitor_id": "...", "monitor_name": "nightly-backup" },
  "fired_at": "2026-10-12T03:14:52.119Z"
}

Verifying the signature

The signature is HMAC-SHA256 over the raw request body, hex-encoded and prefixed with sha256=. Verify it against the raw bytes, before any JSON parsing — re-serialising the body changes the bytes and breaks the comparison.

import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: bytes) -> bool:
    expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)
import crypto from "node:crypto";
import { timingSafeEqual } from "node:crypto";

const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const ok = expected.length === header.length &&
  timingSafeEqual(Buffer.from(expected), Buffer.from(header));

Retries

A non-2xx response or a timeout is retried with exponential backoff, capped at 10 minutes, up to five attempts. Every attempt carries the same X-Pulse-Event-ID, so the receiver can drop duplicates.

  • Answer 2xx as fast as possible and queue the work; the timeout is short.
  • Treat delivery as at-least-once. Deduplicate on event_id.
  • A webhook that has been failing is visible in the alert detail with its last response.
Something missing? Write to support and it gets added here.