Agent skill

ConfigCat Webhooks Skill

Receive and verify ConfigCat webhooks. Use when setting up ConfigCat webhook handlers, debugging X-ConfigCat-Webhook-Signature-V1 verification, or reacting to feature flag and setting changes in a Config and Environment.

Install this skill

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


When to Use This Skill

  • How do I receive ConfigCat webhooks?
  • How do I verify the ConfigCat X-ConfigCat-Webhook-Signature-V1 header?
  • Why is my ConfigCat webhook signature verification failing?
  • How do I react when a feature flag or setting changes in ConfigCat?
  • Why does my ConfigCat webhook fail with two signing keys, or when it uses GET?

Verification (core)

ConfigCat sends X-ConfigCat-Webhook-ID, X-ConfigCat-Webhook-Timestamp (Unix seconds) and X-ConfigCat-Webhook-Signature-V1. The signature is the standard base64 (padded) HMAC-SHA256 of the webhook ID, the timestamp and the raw body, concatenated with no separators. The key is the whole signing key (configcat_whsk_...) as UTF-8 bytes, not decoded. The body may be empty (GET), then only ID + timestamp are signed.

const crypto = require('crypto');

function verify(rawBody, id, timestamp, signature, secret) {
  if (!id || !timestamp || !/^[A-Za-z0-9+\/]{43}=$/.test(signature || '')) return false;
  const message = Buffer.concat([Buffer.from(id, 'latin1'), Buffer.from(timestamp, 'latin1'), rawBody]);
  const expected = Buffer.from(crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(message).digest('base64'));
  const received = Buffer.from(signature);
  return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}
import base64, hashlib, hmac, re

def verify(raw_body: bytes, wid: str, ts: str, signature: str, secret: str) -> bool:
    if not wid or not ts or not re.fullmatch(r"[A-Za-z0-9+/]{43}=", signature or ""):
        return False
    message = wid.encode("latin-1") + ts.encode("latin-1") + raw_body
    digest = hmac.new(secret.encode("utf-8"), message, hashlib.sha256).digest()
    return hmac.compare_digest(base64.b64encode(digest), signature.encode("ascii"))  # compare the base64 text

Verify the raw bytes before parsing (never JSON.parse then re-serialise). Missing headers, a malformed value or a mismatch is a 401. The documented vector (secret configcat_whsk_VN3juirnVh5pNvCKd81RYRYchxUX4j3NykbZG2fAy88=, body examplebody, ID b616ca659d154a5fb907dd8475792eeb, timestamp 1669629035) must produce Ks3cYsu9Lslfo+hVxNC3oQWnsF9e5d73TI5t94D9DRA=.

Timestamp window: old timestamps are not rejected by default. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance. The examples offer an opt-in CONFIGCAT_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off).

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

Hookdeck Setup Note

Keep a single signing key on the ConfigCat webhook and paste it whole, including the configcat_whsk_ prefix. With two keys, ConfigCat sends two signatures and verification fails. If the webhook uses GET, also allow GET on this source.

Hookdeck's source type is CONFIGCAT ("ConfigCat"). It compares the whole header to one signature, as the examples do, so during a key rotation both Hookdeck and the examples reject deliveries until the old key is removed. It pre-selects POST without managing the method, so any other method the sender uses (ConfigCat allows GET or POST) must also be allowed on the source.

Event Types

ConfigCat has no event types. A call means "settings changed" in the Config and Environment the webhook is attached to. The webhook author writes the (optional) body, using ##Variable## placeholders:

PlaceholderReplaced with
##ConfigName## / ##ConfigId##The Config where the change happened
##EnvironmentName## / ##EnvironmentId##The Environment where the change happened
##ChangeNotes##The mandatory notes on the changeset
##ChangedBy##Name and email of the user who made the change
##ChangeDetails##JSON array of settingKey, event, details
##URL##Link to the Config in the dashboard

The examples accept any body (JSON if it parses, else text, or empty) and treat each call as a config-changed notification. With GET there is no body.

For full details, see ConfigCat webhooks.

Important Headers

HeaderDescription
X-ConfigCat-Webhook-IDUnique ID per request; ConfigCat says it "is different for every request", so it can't dedupe ConfigCat's retries
X-ConfigCat-Webhook-TimestampSend time, Unix seconds
X-ConfigCat-Webhook-Signature-V1Base64 HMAC-SHA256, one per signing key, comma-separated

Environment Variables

CONFIGCAT_WEBHOOK_SECRET=configcat_whsk_your_signing_key   # Whole key, including the prefix
CONFIGCAT_WEBHOOK_TOLERANCE_SECONDS=0                      # Optional replay window; unset or 0 = off

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 configcat --path /webhooks/configcat

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →