Webhooks
Receive monitor, incident, and vendor events at your own HTTP endpoint — HMAC-signed, retried, replayable.
Webhooks
Outbound webhooks forward Happy Uptime events to your own infrastructure — internal Slack bots, Datadog, ServiceNow, custom dashboards, CI pipelines. Every payload is HMAC-SHA256 signed, wrapped in a versioned envelope, and recorded in the alert log so you can inspect and replay failed deliveries.
Add a webhook
Dashboard → Alerts → Channels → Add channel → Webhook, or via the API:
bashcurl -X POST https://happyuptime.com/api/v1/alerts/channels \ -H "Authorization: Bearer $HU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Ops bus", "type": "webhook", "config": { "url": "https://hooks.example.com/happyuptime", "headers": { "X-Internal-Tag": "prod" } } }'
The response includes the channel's auto-generated signing_secret (prefix whsec_). Store it — it's the only way to verify request authenticity.
To rotate the secret:
bashcurl -X POST https://happyuptime.com/api/v1/alerts/channels/$CHANNEL_ID/rotate-secret \ -H "Authorization: Bearer $HU_API_KEY"
Subscribing to events
Webhooks receive events based on how they're attached:
-
Monitor events (
monitor.down,monitor.up,monitor.degraded) — attach the channel via an alert rule on a specific monitor, or mark it as default and new monitors auto-link. -
Incident events (
incident.created,incident.updated,incident.resolved,incident.acknowledged) — subscribe at the org level:bashcurl -X POST https://happyuptime.com/api/v1/alerts/project-channels \ -H "Authorization: Bearer $HU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"alert_channel_id": "'"$CHANNEL_ID"'", "alert_type": "incident"}' -
Vendor events (
vendor.down,vendor.degraded,vendor.resolved) — subscribe at the org level withalert_type: "dependency_incident". Per-dependency overrides are also supported in the dependency UI.
Payload envelope
Every webhook POST uses this envelope. The data block varies per event type.
json{ "api_version": "1", "event": "monitor.down", "delivery_id": "whd_8k3nP2oQ1xR7vL9m", "delivered_at": "2026-04-22T14:32:11.812Z", "data": { "monitor": { "id": "mon_abc123", "name": "API", "url": "https://api.example.com/health", "type": "http", "status": "down" }, "check": { "region": "us-east", "regions": null, "status_code": 503, "response_time_ms": 8421, "error": "Service Unavailable" }, "ssl_expiry_days": null, "domain_expiry_days": null, "down_duration": null, "incident": null, "dashboard_url": "https://happyuptime.com/dashboard/monitors/mon_abc123" }}
Incident envelope
json{ "api_version": "1", "event": "incident.resolved", "delivery_id": "whd_9m4oQ3pR2yS8wN0n", "delivered_at": "2026-04-22T14:55:02.004Z", "data": { "incident": { "id": "inc_xyz789", "title": "API elevated error rate", "status": "resolved", "severity": "major", "started_at": "2026-04-22T14:12:03Z", "resolved_at": "2026-04-22T14:55:02Z", "acknowledged_at": "2026-04-22T14:18:44Z", "acknowledged_by": "usr_ops1" }, "update": { "id": "upd_001", "status": "resolved", "message": "Root cause: stale cache node. Drained + restarted.", "is_internal": false, "author_id": "usr_ops1", "created_at": "2026-04-22T14:55:02Z" }, "monitors": [ { "id": "mon_abc123", "name": "API", "status": "up" } ], "dashboard_url": "https://happyuptime.com/dashboard/incidents/inc_xyz789", "status_page_url": null }}
Vendor envelope
json{ "api_version": "1", "event": "vendor.down", "delivery_id": "whd_abc", "delivered_at": "2026-04-22T14:32:11Z", "data": { "vendor": { "name": "AWS", "category": "cloud" }, "incident": { "title": "Increased API error rates — US-EAST-1", "severity": "major", "status": "investigating", "source_url": "https://status.aws.amazon.com/" } }}
Events
| Event | When it fires |
|---|---|
monitor.down | Monitor transitions to down |
monitor.up | Monitor recovers to up |
monitor.degraded | Monitor enters degraded state |
monitor.ssl_expiry | SSL cert within warning window |
monitor.domain_expiry | Domain registration within warning window |
incident.created | Incident opened (manual or auto) |
incident.updated | Status update posted or severity changed |
incident.acknowledged | Someone acknowledged the incident |
incident.resolved | Incident marked resolved |
vendor.down | Tracked vendor (AWS/Stripe/etc.) enters a new incident |
vendor.degraded | Tracked vendor enters a degraded state |
vendor.resolved | Tracked vendor incident closed |
Signature verification
Every request carries three headers:
| Header | Value |
|---|---|
X-Happyuptime-Event | Public event name (e.g. monitor.down) |
X-Happyuptime-Delivery | Unique per-attempt delivery id |
X-Happyuptime-Signature | sha256=<hex> — HMAC-SHA256 of the raw body using your signing_secret |
Verify in Node:
javascriptimport crypto from "crypto";function verify(req, secret) { const header = req.headers["x-happyuptime-signature"]; if (!header) return false; const [algo, hex] = header.split("="); if (algo !== "sha256") return false; const expected = crypto.createHmac("sha256", secret).update(req.rawBody).digest("hex"); const a = Buffer.from(hex); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b);}
Always use a constant-time compare. Never log the secret.
Idempotency
Use X-Happyuptime-Delivery as your idempotency key. Retries (manual or automatic) reuse the same delivery_id from the original envelope, so you can safely process the same payload once.
Retries
The first attempt fires immediately. On any non-2xx response or request timeout (10s), Happy Uptime retries up to 2 more times within the same delivery:
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 1s |
| 3 | 5s |
Only 5xx and 429 responses trigger a retry. 4xx (except 429) is treated as a hard failure — fix the endpoint and replay.
After 3 failed attempts the delivery is marked failed in the alert log. Channels with 50+ consecutive failed deliveries are auto-paused — you'll see a banner in Alerts → Channels. Resume with:
bashcurl -X POST https://happyuptime.com/api/v1/alerts/channels/$CHANNEL_ID/resume \ -H "Authorization: Bearer $HU_API_KEY"
Managed retries via Hookstream
For exponential backoff over hours and automatic DLQ, set config.hookstream_source_id on the channel. Happy Uptime forwards the signed payload to Hookstream, which handles retries, circuit breakers, and observability.
Replay
Every delivery — including its full request body, response code, and response snippet — is stored in the alert log.
-
Dashboard: Alerts → Log → click row → Resend
-
API:
bash# Inspect a deliverycurl https://happyuptime.com/api/v1/alerts/log/$LOG_ID \ -H "Authorization: Bearer $HU_API_KEY"# Replay — re-sends the original signed bodycurl -X POST https://happyuptime.com/api/v1/alerts/log/$LOG_ID/retry \ -H "Authorization: Bearer $HU_API_KEY"
Replays reuse the original delivery_id so your idempotency layer de-dupes automatically. The replay carries an additional header X-Happyuptime-Delivery-Retry: true.
Debugging
- No request arriving? Check Alerts → Log for the attempt. 4xx → your endpoint rejected it (auth, body shape). 5xx or timeout → your service errored or was slow.
- Signature mismatch? Make sure you're hashing the raw request body, not a parsed/re-serialized copy. Whitespace matters.
- Channel paused? 50 consecutive failures trigger auto-pause. Hit
/resumeafter fixing. - Want to test offline? Pipe webhooks through webhook.site or run ngrok in front of localhost.