# Status webhooks

Dwellir's status page can POST status changes to your HTTPS endpoint. Subscribe from the **Subscribe** button on [status.dwellir.com](https://status.dwellir.com).

## Events

| `type` | Sent when |
|---|---|
| `subscription.test` | Once, right after you subscribe. The subscription turns active when this delivery succeeds |
| `incident.opened` | An endpoint becomes degraded or has an outage |
| `incident.updated` | An open incident changes severity or cause |
| `incident.resolved` | The endpoint is operational again |

Severity is `degraded` or `outage`. Cause is `lag` (responses trail the chain head by 30 s or more) or `errors` (failed requests).

If you chose **Outages only**, you receive events for incidents that reached `outage`.

A subscription follows everything, or only the chains, networks, and endpoints you selected. See [subscribe.md](https://status.dwellir.com/subscribe.md) for filter values and the API to create subscriptions.

## Request

```http
POST /your/path HTTP/1.1
Content-Type: application/json
User-Agent: Dwellir-Status-Webhooks/1.0
X-Dwellir-Event: incident.opened
X-Dwellir-Delivery: 1842
X-Dwellir-Signature: t=1791792000,v1=5f2b…
```

```json
{
  "id": "evt_42",
  "type": "incident.opened",
  "created_at": "2026-10-12T08:00:00.000Z",
  "summary": "Degraded: Ethereum Mainnet (api-ethereum-mainnet.n.dwellir.com) is lagging behind the chain head",
  "incident": {
    "id": "6c1d…",
    "status": "open",
    "severity": "degraded",
    "peak_severity": "degraded",
    "cause": "lag",
    "started_at": "2026-10-12T08:00:00.000Z",
    "resolved_at": null
  },
  "component": {
    "chain": { "slug": "ethereum", "title": "Ethereum" },
    "network": { "key": "mainnet", "name": "Mainnet" },
    "endpoint": { "fqdn": "api-ethereum-mainnet.n.dwellir.com", "name": "Ethereum Mainnet" }
  },
  "metrics": { "success_ratio": 1, "lagging_ratio": 0.8, "max_lag_blocks": 57 },
  "links": { "status_page": "https://status.dwellir.com" }
}
```

`id` stays the same across retries. Use it to drop duplicates.

## Verify the signature

`X-Dwellir-Signature` is `t=<unix seconds>,v1=<hex HMAC-SHA256>`. The HMAC covers `"<t>.<raw request body>"` and uses your signing secret (`whsec_…`).

```ts
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verify(rawBody: string, header: string, secret: string, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=') as [string, string]))
  const timestamp = Number(parts.t)
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest()
  const received = Buffer.from(parts.v1 ?? '', 'hex')
  return received.length === expected.length && timingSafeEqual(received, expected)
}
```

Slack and Discord webhook URLs receive formatted messages without a signature.

## Delivery rules

- Respond with any 2xx status within 10 seconds. Redirects count as failures.
- Failed deliveries retry after 30 s, 60 s, 120 s, and so on, capped at 1 hour, for up to 10 attempts (about 3 hours).
- After the last failed attempt, the subscription is disabled. Subscribe again to re-enable it.
- Targets must be public HTTPS hosts on port 443.

## Unsubscribe

Open the unsubscribe link you received when you subscribed and confirm. Removing the Slack or Discord webhook on your side also works: deliveries then fail and the subscription is disabled.
