# Guide to Flagsmith Webhooks: Features and Best Practices

Flagsmith webhooks tell your systems when feature flags change. When someone turns a flag on in production, changes a remote config value, or adds an override for a segment, Flagsmith can post the change to your endpoint, so you can refresh caches, keep other systems in sync, record changes for compliance, or start a pipeline the moment a release flag flips.

This guide covers Flagsmith's three kinds of webhook, their payloads, how they are signed and verified, what happens when your endpoint fails, and the best practices for running a Flagsmith receiver in production.

## What are Flagsmith webhooks?

Flagsmith has three webhook products, configured in three places:

* **Environment webhooks** send flag change events for one environment. You can add any number per environment, and "each event will generate an HTTP POST to all configured webhook URLs".
* **Audit log webhooks** stream every entry in your organisation's audit log, as it happens.
* **Identity webhooks** are an integration that sends the full set of flags, traits and segments for a user every time an SDK evaluates that identity through the Get Identity Flags endpoint.

All three send a JSON `POST`, and all three use the same optional signing. If you set a secret on the webhook, Flagsmith adds an `X-Flagsmith-Signature` header containing a hex HMAC-SHA256 of the raw body.

## Flagsmith webhook features

| Feature | Details |
| --- | --- |
| Configuration | Environment settings (environment webhooks), organisation settings (audit log webhooks), integrations (identity webhooks) |
| Subscription model | No event filter. Each webhook receives every event of its kind |
| Method and format | `POST`, `application/json` |
| Signature header | `X-Flagsmith-Signature`, only when a secret is set |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | The raw request body, nothing else |
| Secret | Optional, chosen by you, used as a UTF-8 string |
| Replay window | None. No timestamp is sent or signed |
| Timeout and retries | Not documented. Flagsmith's code uses a 10 second timeout and retries connection failures only |

Flagsmith publishes no source IP list for webhooks. If you self-host Flagsmith, the requests come from your own servers.

## Common events

Flagsmith's docs list four triggers for environment webhooks and say they are "all sent as `event_type`: `FLAG_UPDATED`":

| Trigger | `event_type` |
| --- | --- |
| Creating a new feature | `FLAG_UPDATED` |
| Updating a feature value or state in an environment | `FLAG_UPDATED` |
| Overriding a feature for an identity | `FLAG_UPDATED` |
| Overriding a feature for a segment | `FLAG_UPDATED` |

Here is Flagsmith's environment webhook example, with `previous_state` trimmed:

```json
{
  "data": {
    "changed_by": "user@domain.com",
    "new_state": {
      "enabled": true,
      "environment": { "id": 23, "name": "Development" },
      "feature": {
        "created_date": "2021-02-10T20:03:43.348556Z",
        "default_enabled": false,
        "description": "Show html in a butter bar for certain users",
        "id": 7168,
        "initial_value": null,
        "name": "butter_bar",
        "project": { "id": 12, "name": "Flagsmith Website" },
        "type": "CONFIG"
      },
      "feature_segment": null,
      "feature_state_value": "<strong>\nYou are using the develop environment.\n</strong>",
      "identity": null,
      "identity_identifier": null,
      "multivariate_feature_state_values": [
        {
          "id": 1,
          "multivariate_feature_option": { "id": 10, "value": "variant_a" },
          "percentage_allocation": 50
        }
      ]
    },
    "previous_state": { "enabled": false },
    "timestamp": "2021-06-18T07:50:26.595298Z"
  },
  "event_type": "FLAG_UPDATED"
}

```

Compare `new_state` with `previous_state` to see what changed. A segment override sets `feature_segment`, and an identity override sets `identity` and `identity_identifier`, so the same `event_type` covers four different changes.

Flagsmith's open-source API code goes further than its docs. It also defines `FLAG_DELETED`, sent with `new_state` set to `null` and the deleted state in `previous_state`, and `NEW_VERSION_PUBLISHED` for environments using feature versioning v2. Handle unknown `event_type` values with a `200` rather than an error.

Audit log webhooks carry an audit log entry: `created_date`, a human-readable `log` such as `"New Flag / Remote Config created: my_feature"`, the `author`, the `environment` and `project`, and `related_object_type` (`FEATURE`, `FEATURE_STATE`, `SEGMENT`, `ENVIRONMENT` and others) with `related_object_id` or `related_object_uuid`. Flagsmith's docs show that entry as the whole body. Its code wraps it as `{"event_type": "AUDIT_LOG_CREATED", "data": {...}}`, and also sends flag change events to organisation webhooks, so check for `event_type` before assuming the shape.

Identity webhooks have no `event_type`. The body is `{ "flags": [...], "identity": "user_test", "segments": [...], "traits": [...] }`, describing what one user saw at evaluation time.

## Setting up Flagsmith webhooks

1. Generate a long random secret, for example with `openssl rand -hex 32`, and store it server-side as `FLAGSMITH_WEBHOOK_SECRET`. Flagsmith does not generate one for you.
2. Add the webhook where it belongs:
  
  * For flag changes, open the environment's settings and add a webhook.
  * For the audit log, add the webhook in your organisation settings.
  * For identity evaluations, add the Webhook integration.
3. Enter your endpoint URL and the secret.
4. Change a flag in a development environment to send a `FLAG_UPDATED` event. Flagsmith's web app also has a test webhook action that sends a sample body.

Give each webhook product its own URL. Identity and audit log bodies are shaped differently, and the path is the most reliable way to tell them apart.

For local development, the [Hookdeck CLI](/docs/cli) (`hookdeck listen 3000 flagsmith --path /webhooks/flagsmith`) gives you a public HTTPS URL that forwards to your local server, with no account required.

## Securing Flagsmith webhooks

Read `X-Flagsmith-Signature`, compute an HMAC-SHA256 of the raw request body with your secret as a UTF-8 key, encode it as hex and compare in constant time. The scheme is the same for all three webhook products.

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

function verifyFlagsmithSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/flagsmith', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyFlagsmithSignature(req.body, req.get('x-flagsmith-signature'), process.env.FLAGSMITH_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.event_type; acknowledge unknown types with a 200.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

def verify_flagsmith_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    received = signature_header.strip().lower().encode("utf-8")
    return hmac.compare_digest(received, expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_flagsmith_signature(raw_body, request.headers.get("x-flagsmith-signature"),
#                            os.environ["FLAGSMITH_WEBHOOK_SECRET"])

```

To check your verifier, use the secret `flagsmith-test-secret` and this exact body:

```
{"data": {"changed_by": "user@domain.com", "timestamp": "2021-06-18T07:50:26.595298Z"}, "event_type": "FLAG_UPDATED"}

```

The signature should be:

```
ba8bb51f9101d22982647a9e379f6f5cf7be107b0880b76c6f7fcc8c27880ab9

```

Flagsmith serialises the body with Python's `json.dumps` and sorted keys, so it has a space after every `:` and `,`. That is one more reason to hash the bytes you received rather than re-serialised JSON.

> **Make Flagsmith webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies `X-Flagsmith-Signature`, accepts every flag change within Flagsmith's 10 second timeout, and durably queues it before your service sees it.

## Flagsmith webhook limitations and pain points

### A failed response is not retried

**The Problem:** Your endpoint returned a `500` during a deploy, and the flag change it carried never arrives again.

**Why It Happens:** Flagsmith's docs do not describe retries. In its open-source code, environment and audit log webhooks are retried only when the request itself fails, such as a connection error or a timeout. A response with a non-2xx status is logged and not retried. Retries run only when Flagsmith uses its task processor, with three attempts and an exponentially growing delay by default, and the organisation is emailed after the last one fails.

**Workarounds:**

* Return `2XX` as soon as the request is verified and stored, and do the real work afterwards.
* After an outage, re-read flag states from the Flagsmith API rather than trusting that you saw every change.
* If you self-host, the retry count and backoff are configurable with `WEBHOOK_BACKOFF_RETRIES` and `WEBHOOK_BACKOFF_BASE`.

**How Hookdeck Can Help:** Hookdeck accepts each request as soon as it arrives, so Flagsmith sees a success, and then retries delivery to your service on [your own schedule](/docs/retries). Anything that failed can be replayed once you have fixed the problem.

### One event type for four kinds of change

**The Problem:** Your handler cannot tell a new feature from a segment override.

**Why It Happens:** Every documented environment trigger is sent as `FLAG_UPDATED`. The difference is only in the data: whether `previous_state` exists, and whether `feature_segment` or `identity` is set.

**Workarounds:**

* Classify changes by inspecting `new_state` and `previous_state` rather than `event_type`.
* Treat a missing `previous_state` as a newly created state.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can route on any body field, for example sending events where `data.new_state.feature_segment` is not null to a segment-aware consumer.

### No secret, no signature

**The Problem:** Requests arrive without `X-Flagsmith-Signature`.

**Why It Happens:** The secret is optional on every webhook, and without it Flagsmith sends no signature.

**Workarounds:**

* Always set a secret and reject unsigned requests.
* Set it on all three webhook products, since each has its own.

**How Hookdeck Can Help:** Hookdeck verifies the signature when you give the source your secret. If you didn't set a secret in Flagsmith, leave verification off on the source.

### Identity webhooks are high volume

**The Problem:** Your endpoint is flooded once identity webhooks are turned on.

**Why It Happens:** An identity webhook fires every time an SDK calls Get Identity Flags, which can be every page load or app start. It is evaluation telemetry, not change notification.

**Workarounds:**

* Point identity webhooks at a separate endpoint that only writes to a queue or warehouse.
* Batch inserts downstream instead of processing each request inline.

**How Hookdeck Can Help:** Hookdeck absorbs the burst and can cap the delivery rate to your destination, so your warehouse loader sees a steady stream instead of your traffic peaks.

### No timestamp means no replay protection

**The Problem:** A captured request verifies if it is sent again later.

**Why It Happens:** The signature covers only the body, and no timestamp header is sent.

**Workarounds:**

* Make handlers idempotent.
* Use the body's `data.timestamp` to ignore a change older than the state you already hold.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated requests based on fields you choose, such as `data.timestamp` and the feature ID.

## Best practices

### Verify against the raw body

Mount `express.raw({ type: '*/*' })` in Express, use `await request.text()` in a Next.js route handler, or read `await request.body()` in FastAPI. Parse only after verification.

### Use one URL per webhook product

Environment, audit log and identity bodies have different shapes, and identity bodies have no `event_type` at all. A path per product, such as `/webhooks/flagsmith/environment`, keeps the routing obvious.

### Treat flag changes as cache invalidations

The most common use of environment webhooks is refreshing a flag cache. Use the webhook to know that something changed, and fetch the current environment state from Flagsmith if you need it to be complete, especially after any period of downtime.

### Build your own idempotency key

There is no event ID. For environment webhooks, combine the environment ID, feature ID, any segment or identity, and `data.timestamp`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Flagsmith's code waits 10 seconds and does not follow redirects. Verify, store and respond, then process from a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Flagsmith sends environment, audit log and identity webhooks, all signed the same way when you set a secret. Verify `X-Flagsmith-Signature` against the raw bytes, give each product its own URL, compare `new_state` with `previous_state` to see what changed, and respond with a `2XX` quickly, because a failed response is not sent again.

[Hookdeck Event Gateway](https://hookdeck.com) checks every signature, accepts each flag change straight away and holds it until your service has processed it, so a deploy on your side does not leave a stale flag in your cache.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Flagsmith webhooks reliably in minutes.