A webhook consumer done right.
Four things make a webhook receiver production-shaped: verify the signature over the raw body, reject stale timestamps, dedupe by delivery id, and answer 2xx fast — do the real work after responding. This is the whole thing in Node; the signature math in Go lives in the webhooks guide.
Subscribe
curl -s -X POST https://app.upfour.io/v1/webhook-subscriptions \
-H "Authorization: Bearer $UP4_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://ops.example.com/hooks/up4",
"events": ["monitor.status_changed", "incident.opened", "incident.resolved"]
}'
The response includes the subscription secret — store it next to your other secrets.
Then send yourself a test delivery:
POST /webhook-subscriptions/{id}/test.
The receiver (Node)
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.UP4_WEBHOOK_SECRET;
const seen = new Set(); // use your store in production
function verify(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; // replay window
const expected = crypto.createHmac("sha256", SECRET)
.update(`${ts}.`).update(rawBody).digest("hex");
try {
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
} catch { return false; }
}
// express.raw, NOT express.json — the signature covers the raw bytes.
app.post("/hooks/up4", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.header("X-Up4-Signature") ?? "", req.body)) {
return res.status(401).end();
}
const deliveryId = req.header("X-Up4-Delivery");
if (seen.has(deliveryId)) return res.status(200).end(); // dedupe redeliveries
seen.add(deliveryId);
res.status(200).end(); // answer fast; work after
const event = req.header("X-Up4-Event");
const payload = JSON.parse(req.body);
switch (event) {
case "incident.opened":
// page your own systems, open a ticket, dim the lights …
break;
case "monitor.status_changed":
// update your own dashboard …
break;
}
});
app.listen(8787);Test it end to end — safely
Sandbox webhooks still deliver for real (only notifications are suppressed), so a
pls_test_ key plus a sandbox monitor pointed at a URL you
control gives you the full loop: break the URL, watch
incident.opened arrive, fix it, watch
incident.resolved. Inspect and redeliver any delivery via
GET /webhook-subscriptions/{id}/deliveries.
Notes
Destinations must be publicly reachable — deliveries refuse loopback, private ranges and metadata endpoints (SSRF protection). For local development, a tunnel (cloudflared, ngrok) works fine.