# Prometheus Alertmanager Webhooks

Alertmanager POSTs a version 4 JSON notification to each URL in a receiver's `webhook_configs`
whenever a group of alerts fires or resolves. Webhooks are **not signed**. The receiver's
`http_config` sets an `Authorization` header on every request, either `Bearer <credentials>`
(`authorization`, default type Bearer) or `Basic base64(username:password)` (`basic_auth`). Your
endpoint authenticates the request by comparing those credentials with the ones you configured.

Hookdeck's Prometheus Alertmanager source offers both: **Basic auth** (the default) for
`basic_auth`, where you enter the username and password, and **Bearer token** for `authorization`,
where you enter the bare credentials.

Setup note (same as Hookdeck's Prometheus Alertmanager source): "Set basic_auth, or authorization with the default Bearer type, in the webhook receiver's http_config. OAuth 2.0 is not supported."

## When to Use This Skill

* How do I receive Prometheus Alertmanager webhooks?
* How do I authenticate requests from an Alertmanager `webhook_configs` receiver?
* Does Alertmanager sign webhook payloads? (No: check the `Authorization` credentials, Bearer or basic_auth)
* How do I handle `firing` and `resolved` alerts and loop over `alerts[]`?
* How do I deduplicate repeated Alertmanager notifications?
* Why does my Alertmanager webhook endpoint return 401?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

const BEARER = /^bearer /i; // auth schemes are case-insensitive (RFC 9110)
const BASIC = /^basic +([A-Za-z0-9+/]+={0,2})$/i;

function verifyAlertmanagerRequest(headers, env = process.env) {
  const header = headers['authorization'];
  if (typeof header !== 'string' || !header) return false;

  if (BEARER.test(header)) {                         // http_config.authorization (Bearer)
    const expected = (env.ALERTMANAGER_WEBHOOK_SECRET || '').replace(BEARER, ''); // bare secret
    return Boolean(expected) && safeEqual(header.replace(BEARER, ''), expected);
  }

  const m = BASIC.exec(header);                      // http_config.basic_auth
  const user = env.ALERTMANAGER_WEBHOOK_USERNAME;
  const pass = env.ALERTMANAGER_WEBHOOK_PASSWORD;
  if (!m || !user || !pass) return false;            // not Basic, or basic_auth not configured
  const buf = Buffer.from(m[1], 'base64');
  if (buf.toString('base64') !== m[1]) return false; // canonical base64 only
  const decoded = buf.toString('utf8');
  const i = decoded.indexOf(':');                    // FIRST colon: a password may contain ':'
  if (i === -1) return false;
  const userOk = safeEqual(decoded.slice(0, i), user);
  const passOk = safeEqual(decoded.slice(i + 1), pass); // evaluate both, no short-circuit
  return userOk && passOk;
}

```

* `authorization`: `Authorization: Bearer <credentials>`. Store just the secret (no `Bearer `
  prefix) in `ALERTMANAGER_WEBHOOK_SECRET`; a leading `Bearer ` in the configured value is
  stripped. Do not trim or otherwise alter the secret.
* `basic_auth`: `Authorization: Basic base64(username:password)`, checked against
  `ALERTMANAGER_WEBHOOK_USERNAME` and `ALERTMANAGER_WEBHOOK_PASSWORD`. Split on the first colon (a
  password may contain `:`) and compare both parts. The `Basic` scheme matches in any case.
  Hookdeck never sends a 401 challenge; Alertmanager sends `basic_auth` on every request anyway.
* A form you haven't configured is rejected, so with neither configured every request is rejected.
* Return `401` for a missing header, wrong scheme or mismatch, `400` for a body that isn't a JSON
  object, `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and authenticate
  before parsing it.
* Python: `hmac.compare_digest` on bytes (encode both sides; passing `str` raises on non-ASCII).

**A static shared credential authenticates the sender, not the body, and gives no replay
protection** (there is no signature and no timestamp). Serve the endpoint over HTTPS and rotate the
credential if it leaks.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/prometheus-alertmanager-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/prometheus-alertmanager-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/prometheus-alertmanager-webhooks/examples/fastapi/).

## Payload and Statuses

Alertmanager has no event-type field. The body is a version `"4"` notification:
`version`, `groupKey`, `truncatedAlerts`, `status`, `receiver`, `groupLabels`, `commonLabels`,
`commonAnnotations`, `externalURL`, and `alerts[]` (each with `status`, `labels`, `annotations`,
`startsAt`, `endsAt`, `generatorURL`, `fingerprint`).

| Status | Meaning |
| --- | --- |
| `firing` | The alert (or group) is active |
| `resolved` | The alert is no longer active (sent when `send_resolved` is true, the default) |

`status` is a state, not an event type, and the group `status` is `firing` while any alert in it is
firing. Loop over `alerts[]` and use each alert's own `status`.

Notifications repeat every `group_interval` and `repeat_interval`. Deduplicate with `groupKey`
plus each alert's `fingerprint` (and `status`, `startsAt`, `endsAt` if resolved should count as new).

## Environment Variables

```bash
# authorization (Bearer): the bare credentials from alertmanager.yml
ALERTMANAGER_WEBHOOK_SECRET=your-shared-secret
# basic_auth: the username and password from alertmanager.yml
ALERTMANAGER_WEBHOOK_USERNAME=alertmanager
ALERTMANAGER_WEBHOOK_PASSWORD=your-password

```

## Local Development

Alertmanager can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 prometheus-alertmanager --path /webhooks/prometheus-alertmanager

```

Use the printed URL as the receiver's `url`.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/prometheus-alertmanager-webhooks/references/overview.md) - What Alertmanager webhooks are, payload shape and statuses
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/prometheus-alertmanager-webhooks/references/setup.md) - `alertmanager.yml` receiver, Bearer vs basic_auth
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/prometheus-alertmanager-webhooks/references/verification.md) - Credential check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/prometheus-alertmanager-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests