Agent skill

ClickHouse (ClickStack) Webhooks Skill

Receive and authenticate ClickHouse ClickStack (HyperDX) alert webhooks. Use when setting up an endpoint for a ClickStack Generic alert webhook, checking the secret header you configured, debugging 401s, or handling alert payloads built from the {{title}}, {{body}} and {{link}} template variables.

Install this skill

npx skills add hookdeck/webhook-skills --skill clickhouse-webhooks


Covers ClickStack alert webhooks of type Generic only. ClickStack (the HyperDX observability stack) can call a URL when a search alert or dashboard chart alert fires. ClickHouse Cloud's control plane has no event webhooks.

Generic alert webhooks are not signed. The only authentication is a secret header you add yourself under Webhook headers when you create the webhook. Your endpoint compares that header with the value you stored.

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential if it leaks.

When to Use This Skill

  • How do I receive ClickStack (ClickHouse) alert webhooks?
  • How do I authenticate a Generic webhook from ClickStack if it has no signature?
  • How do I handle the {{title}}, {{body}} and {{link}} template variables?
  • Why does my ClickStack alert webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
const digest = (v) => crypto.createHash('sha256').update(String(v)).digest();

function verifyClickHouseRequest(headers, env = process.env) {
  const secret = env.CLICKHOUSE_WEBHOOK_SECRET;
  if (!secret) return false;                          // never accept when unconfigured
  const name = (env.CLICKHOUSE_WEBHOOK_HEADER || 'Authorization').toLowerCase();
  const received = headers[name];                     // Node lowercases header names
  // Compare the WHOLE value (including any "Bearer " you typed); do not parse it.
  return Boolean(received) && crypto.timingSafeEqual(digest(received), digest(secret));
}
  • The header name is whatever you typed in Webhook headers; read it from CLICKHOUSE_WEBHOOK_HEADER (default Authorization) and look it up case-insensitively.
  • CLICKHOUSE_WEBHOOK_SECRET is the exact, whole header value. If you entered Bearer abc123, the secret is Bearer abc123. Do not strip or parse the prefix.
  • Return 401 for a missing header or mismatch, 200 once handled.
  • Method and content type are not documented. Read the body raw (express.raw({ type: '*/*' })), authenticate first, then parse it. A body that isn't JSON is accepted as plain text.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Payload

The body is an optional template you write; ClickStack adds no event field and ClickHouse documents no event names. Generic webhooks support these template variables:

VariableContains
{{title}}Alert title
{{body}}Alert description
{{link}}Link to the search or chart the alert is attached to

The examples use this illustrative body template (yours may differ):

{"title": "{{title}}", "body": "{{body}}", "link": "{{link}}"}

Environment Variables

CLICKHOUSE_WEBHOOK_HEADER=Authorization        # header name you added under Webhook headers
CLICKHOUSE_WEBHOOK_SECRET=your-secret-value    # the exact, whole header value

Local Development

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

npx hookdeck-cli listen 3000 clickhouse --path /webhooks/clickhouse

Use the printed URL as the Webhook URL, and add your secret header under Webhook headers.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →