---
title: Webhook alerts
description: Send alerts to your own endpoint, with HMAC signing, custom headers and delivery IDs for deduplication.
sidebar:
  icon: webhook
  label: Webhook
---

{/* Source of truth: packages/shared/src/notifications.ts —
    webhookChannelSchema (url, secret, headers).
    packages/api/src/notifications/infrastructure/dispatchers/
    webhook.dispatcher.ts — body shape, headers, HMAC, 10s timeout,
    redirect: "manual", dispatch-time SSRF revalidation.
    packages/shared/src/http.ts — GlowoHeaders. */}

A webhook channel posts alerts to a URL you control, so you can drive your own
automation — open a ticket, flip a feature flag, page through a system Glowo
does not integrate with directly.

## Configuration

| Field | Value | Required |
| --- | --- | --- |
| `url` | The endpoint to POST to | yes |
| `secret` | A signing secret; enables `X-Glowo-Signature` | no |
| `headers` | Extra headers to send | no |

## The request

`POST` with `Content-Type: application/json`.

| Header | Meaning |
| --- | --- |
| `X-Glowo-Delivery-Id` | Stable across retries of the same logical delivery |
| `X-Glowo-Event` | The event name; currently always `monitor.status_changed` |
| `X-Glowo-Attempt` | Which attempt this is |
| `X-Glowo-Signature` | Hex HMAC-SHA-256 of the raw body — only when `secret` is set |

Body:

```json
{
  "event": "monitor.status_changed",
  "monitor": { "name": "API", "url": "https://api.example.com/health" },
  "previousStatus": "UP",
  "newStatus": "DOWN",
  "latency": 0,
  "statusMessage": "CONNECTION_REFUSED",
  "downtimeDurationMs": 0,
  "timestamp": "2026-08-01T09:15:00.000Z",
  "viewUrl": "https://glowo.dev/app/monitors/..."
}
```

## Deduplicate on the delivery ID

**Your receiver must be idempotent.** Delivery is at-least-once: a request that
times out, or that your endpoint fails to acknowledge, is retried.

`X-Glowo-Delivery-Id` is stable across every retry of the same logical delivery
— only `X-Glowo-Attempt` changes. Store seen delivery IDs and ignore repeats.
Without that, a slow response on your side turns one outage into three tickets.

## Verifying the signature

When `secret` is set, `X-Glowo-Signature` is the hex HMAC-SHA-256 of the **raw
request body**. Compute it over the bytes you received, before any JSON parsing
and re-serialisation — re-encoding changes the bytes and the signature will not
match.

```js
import { createHmac, timingSafeEqual } from "node:crypto";

const verify = (rawBody, signature, secret) => {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signature, "hex");
  // Length check first: timingSafeEqual throws on a length mismatch.
  return a.length === b.length && timingSafeEqual(a, b);
};
```

Use a constant-time comparison, not `===`.

## Delivery behaviour

- **10 second timeout.** Respond quickly — acknowledge with a `2xx` and do the
  real work asynchronously. Slow endpoints get retried, which means duplicates.
- **Redirects are not followed.** Give Glowo the final URL.
- **The target is re-validated at dispatch time**, not only when you save the
  channel. A URL that resolves to a private, loopback or cloud-metadata address
  is refused — this closes a DNS-rebinding hole, and it means a webhook pointing
  into a private network will not work by design.
- Failures are retried and eventually dead-lettered, with a per-channel circuit
  breaker. See [Delivery history](/alerting/delivery-history).

## Custom headers

`headers` are sent with every request — useful for an API key or a routing
header your receiver expects. They are sanitised before sending, and cannot
override the `X-Glowo-*` headers above.

:::caution
`X-Glowo-*` header names and the body fields are a public contract. Renaming one
would break every receiver, so treat additions as the only safe change — and
write your receiver to ignore fields it does not recognise.
:::
