> ## Documentation Index
> Fetch the complete documentation index at: https://docs.happyuptime.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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:

```bash
curl -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:

```bash
curl -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:

  ```bash
  curl -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 with `alert_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:

```javascript
import 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](#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:

```bash
curl -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 delivery
  curl https://happyuptime.com/api/v1/alerts/log/$LOG_ID \
    -H "Authorization: Bearer $HU_API_KEY"

  # Replay — re-sends the original signed body
  curl -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 `/resume` after fixing.
- **Want to test offline?** Pipe webhooks through [webhook.site](https://webhook.site) or run [ngrok](https://ngrok.com) in front of localhost.