Gareth Wilson Gareth Wilson

Guide to ConfigCat Webhooks: Features and Best Practices

Published


ConfigCat webhooks tell your systems that a feature flag or setting has changed. Someone clicks Save & Publish Changes in the ConfigCat dashboard, and within moments ConfigCat calls a URL you chose. Applications use that call to refresh their cached flag values straight away instead of waiting for the next poll, and teams use it to post change notices to chat or trigger a deploy.

This guide covers how ConfigCat webhooks are configured, what the request body contains, how requests are signed and why signing key rotation needs care, and the practices that keep a ConfigCat receiver reliable.

What are ConfigCat webhooks?

A ConfigCat webhook is tied to one Config and one Environment. When settings in that pair change, ConfigCat sends an HTTP request to the webhook's URL with the method you picked, GET or POST. You can add custom headers, and you can write the request body yourself, with placeholders such as ##ConfigName## and ##ChangeDetails## that ConfigCat fills in at send time.

That makes ConfigCat different from most webhook providers. There are no event types and no fixed payload. Every request means "settings changed here", and the body is whatever template you wrote, or nothing at all.

Each request is signed. ConfigCat concatenates the X-ConfigCat-Webhook-ID value, the X-ConfigCat-Webhook-Timestamp value and the raw body, with no separators, computes an HMAC-SHA256 with your signing key and sends the base64 result in X-ConfigCat-Webhook-Signature-V1.

ConfigCat webhook features

FeatureDetails
ConfigurationDashboard: Webhooks screen, + ADD WEBHOOK
TriggerChanges saved and published in the chosen Config and Environment
HTTP methodGET or POST, set per webhook
Request bodyOptional, written by you, with ##Variable## placeholders
Custom headersOptional, set per webhook (not signed)
Signature headerX-ConfigCat-Webhook-Signature-V1: comma-separated base64 signatures, one per signing key
Other signed headersX-ConfigCat-Webhook-ID (different for every request), X-ConfigCat-Webhook-Timestamp (Unix seconds)
SchemeHMAC-SHA256, base64
Signed content{webhook ID}{timestamp}{raw body}, no separators
Signing keysconfigcat_whsk_..., on the webhook's page, with an optional secondary key for rotation
RetriesUp to 5 retries with variable delays, on network issues or a non-2xx response

ConfigCat doesn't publish a response timeout or source IP addresses. It recommends a timestamp check but doesn't set a window.

The request body

Because you write the body, you decide what your receiver gets. These placeholders are available:

PlaceholderReplaced with
##ConfigName##, ##ConfigId##The Config where the change happened
##EnvironmentName##, ##EnvironmentId##The Environment where the change happened
##URL##A link to the Config in the ConfigCat dashboard
##ChangeNotes##The mandatory notes on the changeset
##ChangedBy##The name and email address of the person who made the change
##ChangeDetails##A JSON array of changes: setting key, event and details
##ChangeDetailsTeams##, ##ChangeDetailsSlack##The change details formatted for Microsoft Teams or Slack

A body that a service of your own can parse might look like this:

{
  "config": "##ConfigName##",
  "configId": "##ConfigId##",
  "environment": "##EnvironmentName##",
  "changedBy": "##ChangedBy##",
  "changes": ##ChangeDetails##
}

##ChangeDetails## becomes a JSON array, so it goes in without quotes. ConfigCat's own example of it:

[
  {
    "settingKey": "myAwesomeFeature1",
    "event": "changed",
    "details": "\r\nmyAwesomeFeature1: false ➔ true"
  }
]

The details text is meant for people, with line breaks and arrows. Treat the array as a list of keys that changed, and read the current values from ConfigCat rather than parsing details.

Setting up ConfigCat webhooks

  1. In the ConfigCat dashboard, open the Webhooks screen and click + ADD WEBHOOK.
  2. Select the Config and Environment whose changes should trigger it.
  3. Set the URL and the HTTP method. POST is the usual choice when you send a body.
  4. Optionally add custom headers and a request body.
  5. Copy the signing key from the webhook's page and store it, for example as CONFIGCAT_WEBHOOK_SECRET.
  6. Test it: change a setting, click SAVE & PUBLISH CHANGES, and check that your endpoint was called.

If you use Hookdeck, create a source with the ConfigCat source type and use its URL as the webhook URL. 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. ConfigCat webhooks can use GET or POST, so the Hookdeck source pre-selects POST without fixing it: if your webhook uses GET, also allow GET on the source.

Securing ConfigCat webhooks

Read the three headers. Concatenate the webhook ID, the timestamp and the raw body as bytes, with nothing between them. If the webhook has no body, sign the ID and timestamp alone. Compute an HMAC-SHA256 keyed with the whole signing key, configcat_whsk_ prefix included, encode it as standard base64 and compare it with the header in constant time. Base64 is case-sensitive, so compare it exactly.

const crypto = require('crypto');
const express = require('express');

function verifyConfigCatSignature(rawBody, webhookId, timestamp, signature, secret) {
  if (!webhookId || !timestamp || !signature || !secret) return false;
  // One signing key means one 44-character base64 digest. Two keys send a comma-separated list.
  if (!/^[A-Za-z0-9+/]{43}=$/.test(signature)) return false;

  const expected = crypto
    .createHmac('sha256', secret) // the whole configcat_whsk_... key, as a string
    .update(webhookId, 'latin1')
    .update(timestamp, 'latin1')
    .update(rawBody)
    .digest('base64');

  const received = Buffer.from(signature, 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

// ConfigCat sends GET or POST. A GET has no body, so sign an empty buffer.
app.all('/webhooks/configcat', express.raw({ type: '*/*' }), (req, res) => {
  if (req.method !== 'GET' && req.method !== 'POST') return res.sendStatus(405);

  const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
  const ok = verifyConfigCatSignature(
    rawBody,
    req.get('x-configcat-webhook-id'),
    req.get('x-configcat-webhook-timestamp'),
    req.get('x-configcat-webhook-signature-v1'),
    process.env.CONFIGCAT_WEBHOOK_SECRET
  );
  if (!ok) return res.sendStatus(401);

  // Settings changed in this Config and Environment: refresh your cached flags.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac
import re

SIGNATURE_PATTERN = re.compile(r"[A-Za-z0-9+/]{43}=")


def verify_configcat_signature(raw_body: bytes, webhook_id, timestamp, signature, secret: str) -> bool:
    if not webhook_id or not timestamp or not signature or not secret:
        return False
    if not SIGNATURE_PATTERN.fullmatch(signature):
        return False

    signed = webhook_id.encode("latin-1") + timestamp.encode("latin-1") + (raw_body or b"")
    digest = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
    expected = base64.b64encode(digest)
    return hmac.compare_digest(signature.encode("utf-8"), expected)


# In FastAPI: raw_body = await request.body()
# verify_configcat_signature(raw_body, request.headers.get("x-configcat-webhook-id"),
#                            request.headers.get("x-configcat-webhook-timestamp"),
#                            request.headers.get("x-configcat-webhook-signature-v1"),
#                            os.environ["CONFIGCAT_WEBHOOK_SECRET"])

To check your code, run ConfigCat's published example through it. With the signing key configcat_whsk_VN3juirnVh5pNvCKd81RYRYchxUX4j3NykbZG2fAy88=, the webhook ID b616ca659d154a5fb907dd8475792eeb, the timestamp 1669629035 and the body examplebody, the signature is:

Ks3cYsu9Lslfo+hVxNC3oQWnsF9e5d73TI5t94D9DRA=

The code above treats the whole header as one signature, which is what Hookdeck's source does too. If you verify only in your own service and want to keep deliveries flowing while two keys are set, split the header on commas and accept a match on any entry.

ConfigCat suggests rejecting old timestamps but names no window, so the samples don't check age. If you add a check, make it optional: ConfigCat retries failed requests, and Hookdeck retries and replays resend the original headers, so a tight window rejects legitimate redeliveries.

Hookdeck's ConfigCat source type verifies HMAC-SHA256 over {X-ConfigCat-Webhook-ID}{X-ConfigCat-Webhook-Timestamp}{body}, base64, against the whole X-ConfigCat-Webhook-Signature-V1 value, and applies no timestamp tolerance. A request that fails the check is marked as failed verification and is not delivered to your destinations.

Make ConfigCat webhooks production-ready. Hookdeck Event Gateway verifies the X-ConfigCat-Webhook-Signature-V1 header on every flag change, stores each request, and fans it out to every service that needs to refresh its configuration.

ConfigCat webhook limitations and pain points

Key rotation sends two signatures

The Problem: Verification starts failing on every request as soon as someone adds a secondary signing key.

Why It Happens: For rotation, ConfigCat signs each request with both keys and sends sigA,sigB in X-ConfigCat-Webhook-Signature-V1. A verifier that treats the header as one digest, Hookdeck's source included, no longer finds a match.

Workarounds:

  • Keep one signing key on the webhook. To rotate, generate the new key, update your receiver, then remove the old key, accepting that requests between those steps fail verification.
  • In your own code, split the header on commas and accept any matching entry.

How Hookdeck Can Help: Requests that fail during a rotation are recorded in Hookdeck as failed verification rather than reaching your services, so you can see exactly when the second key was added.

There are no events to route on

The Problem: Your handler can't tell what changed, or even which Config it is about.

Why It Happens: ConfigCat sends no event type, and the body is only what you put in the template. A webhook with no body tells you nothing but "something changed".

Workarounds:

  • Write a JSON body with ##ConfigId##, ##EnvironmentId## and ##ChangeDetails##.
  • Use one webhook per Config and Environment pair, so the URL or a custom header tells you the scope.

How Hookdeck Can Help: Hookdeck filters can route requests on fields in your template body, so changes to different Configs or Environments can go to different destinations.

GET or POST, and an empty body

The Problem: A webhook set to GET is rejected by a route that only accepts POST, or fails verification because the code expects a body.

Why It Happens: ConfigCat lets each webhook use GET or POST. A GET has no body, so only the ID and timestamp are signed.

Workarounds:

  • Use POST with a body unless you have a reason not to.
  • If you use GET, accept it on your route and sign an empty body. With Hookdeck, also allow GET on the source.

How Hookdeck Can Help: The Hookdeck source lets you choose which methods it accepts, so it can match how each ConfigCat webhook is set up.

Up to five retries

The Problem: An outage longer than ConfigCat's retries means your services keep stale flag values until the next change or poll.

Why It Happens: ConfigCat retries a failed request up to 5 times with variable delays, and doesn't say how long that takes.

Workarounds:

  • Keep your SDKs polling as a fallback, so a missed webhook only delays an update.
  • Return 2xx quickly after verification.

How Hookdeck Can Help: Hookdeck accepts the request as soon as ConfigCat sends it and then delivers to your services with retries you configure.

Best practices

Verify against the raw body

The body is part of the signed content, so read it as raw bytes: express.raw({ type: '*/*' }) in Express, Buffer.from(await request.arrayBuffer()) in Next.js, await request.body() in FastAPI. Treat a missing body as empty bytes.

Make the handler idempotent

ConfigCat describes X-ConfigCat-Webhook-ID as different for every request, so don't rely on it to spot a retry. Most ConfigCat handlers refresh configuration, which is safe to repeat. Design yours so that running it twice has no extra effect. See our guide to webhook idempotency.

Keep polling as a fallback

ConfigCat describes webhooks as adding speed to the reliability of polling. Leave your SDK's polling on so a lost request only delays an update rather than losing it.

Don't trust unsigned headers

Custom headers you add to the webhook are not part of the signature. Use them for routing hints, not for authentication.

Conclusion

ConfigCat signs each webhook with a base64 HMAC-SHA256 of the webhook ID, timestamp and raw body, concatenated with no separators and keyed with the whole configcat_whsk_ key. Write a body that tells your service what changed, verify against the raw bytes, keep a single signing key unless your own code accepts a list, and allow GET wherever a webhook uses it.

Hookdeck Event Gateway verifies every ConfigCat signature, stores each change notification and delivers it to every service that needs to refresh.

Get started with Hookdeck for free and handle ConfigCat webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.