Agent skill

Prometheus Alertmanager Webhooks Skill

Receive and authenticate Prometheus Alertmanager webhooks. Use when setting up an Alertmanager webhook_configs receiver, verifying the Authorization header from http_config (Bearer credentials or basic_auth), debugging 401s, or handling firing and resolved alert groups.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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/, examples/nextjs/, 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).

StatusMeaning
firingThe alert (or group) is active
resolvedThe 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

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

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

Use the printed URL as the receiver's url.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →