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_(defaultWEBHOOK_ HEADER Authorization) and look it up case-insensitively. CLICKHOUSE_is the exact, whole header value. If you enteredWEBHOOK_ SECRET Bearer abc123, the secret isBearer abc123. Do not strip or parse the prefix.- Return
401for a missing header or mismatch,200once 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:
| Variable | Contains |
|---|---|
{{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
- references/overview.md - What ClickStack alert webhooks are and the payload
- references/setup.md - Create a Generic webhook with a secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests