# 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.

```javascript
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);
}

```

```python
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/configcat-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/configcat-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/configcat-webhooks/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:

| Placeholder | Replaced 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](https://configcat.com/docs/advanced/notifications-webhooks/).

## Important Headers

| Header | Description |
| --- | --- |
| `X-ConfigCat-Webhook-ID` | Unique ID per request; ConfigCat says it "is different for every request", so it can't dedupe ConfigCat's retries |
| `X-ConfigCat-Webhook-Timestamp` | Send time, Unix seconds |
| `X-ConfigCat-Webhook-Signature-V1` | Base64 HMAC-SHA256, one per signing key, comma-separated |

## Environment Variables

```bash
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

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/configcat-webhooks/references/overview.md) - ConfigCat webhook concepts and payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/configcat-webhooks/references/setup.md) - Configuration guide
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/configcat-webhooks/references/verification.md) - Signature verification details