API · webhooks

Push, signed, verifiable.

Subscribe once and every state change comes to you — signed, so nobody can forge an “all clear”. Manage subscriptions under /webhook-subscriptions, including a test delivery and per-delivery inspection with redelivery.

Events

monitor.createdmonitor.updatedmonitor.deletedmonitor.status_changed
incident.openedincident.acknowledgedincident.resolvedincident.reopenedincident.escalated
heartbeat.missedheartbeat.recovered
status_page.update_publishedwebhook.test

Delivery headers

X-Up4-EventThe event name, e.g. monitor.status_changed
X-Up4-DeliveryUnique delivery id — use it for dedupe
X-Up4-Signaturet=<unix>,v1=<hmac-sha256-hex>

Verify the signature

HMAC-SHA256 over <timestamp>.<raw_body> with your subscription secret. Reject timestamps outside a five-minute window.

Node

import crypto from "node:crypto";

export function verify(secret, header, rawBody, now = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const ts = Number(parts.t);
  if (!ts || Math.abs(now - ts) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.`)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}

Go

func Verify(secret, header string, body []byte, now time.Time) bool {
	parts := map[string]string{}
	for _, part := range strings.Split(header, ",") {
		k, v, ok := strings.Cut(part, "=")
		if ok {
			parts[k] = v
		}
	}
	ts, err := strconv.ParseInt(parts["t"], 10, 64)
	if err != nil || math.Abs(now.Sub(time.Unix(ts, 0)).Seconds()) > 300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(fmt.Sprintf("%d.", ts)))
	mac.Write(body)
	got, err := hex.DecodeString(parts["v1"])
	return err == nil && hmac.Equal(got, mac.Sum(nil))
}

Destinations

Deliveries refuse loopback, link-local, private RFC 1918 and cloud-metadata destinations (SSRF protection). Test consumers end to end with a sandbox key — sandbox webhooks still deliver for real.