# Guide to Statsig Webhooks: Features and Best Practices

Statsig Event Webhooks notify your systems about experimentation and configuration activity: a config or gate changes, an exposure is logged. If you're auditing config changes, syncing experiment data, or triggering workflows off Statsig activity, webhooks are how you react without polling.

This guide covers how Statsig's Event Webhook (the "Generic Webhook" integration) works, the events you'll handle, how to verify the `X-Statsig-Signature`, the batched payload shape, and the best practices for production.

## What are Statsig webhooks?

Statsig's Event Webhook delivers HTTP POSTs to a destination URL you configure, each carrying one or more events as a JSON batch. Statsig signs deliveries with a Slack/Stripe-style HMAC scheme: an `X-Statsig-Request-Timestamp` header and an `X-Statsig-Signature` header computed over a versioned basestring.

## Statsig webhook features

| Feature | Details |
| --- | --- |
| Configuration | Project Settings > Integrations > Generic Webhook |
| Signature headers | `X-Statsig-Request-Timestamp`, `X-Statsig-Signature` |
| Signature scheme | HMAC-SHA256 over `v0:{timestamp}:{raw body}`, hex, prefixed `v0=` |
| Signing secret | On the Webhook integration card in Project Settings |
| Payload | JSON batches (arrays); config changes wrapped as `{"data": [...]}` |
| Event filtering | Choose Exposures vs Config Changes |
| SDK | No official webhook-verification SDK |

## Common events

Statsig events use a `statsig::` prefix:

| Event | Fires when |
| --- | --- |
| `statsig::config_change` | A config, gate, or experiment definition changes |
| `statsig::gate_exposure` | A feature gate is evaluated (exposure) |
| `statsig::config_exposure` | A dynamic config is evaluated |
| `statsig::experiment_exposure` | An experiment is evaluated |

Config-change payloads carry `type`/`name`/`description`/`action` metadata (with `action` values like `created`, `updated`). Use Event Filtering to pick exposures vs config changes so you only receive what you process.

## Setting up Statsig webhooks

Configure the webhook under Project Settings > Integrations > Generic Webhook: enter your destination URL, copy the signing secret from the integration card, and use Event Filtering to select Exposures and/or Config Changes.

## Securing Statsig webhooks

Each delivery carries `X-Statsig-Request-Timestamp` and `X-Statsig-Signature`. To verify, build the basestring `v0:{timestamp}:{raw body}`, compute an HMAC-SHA256 with your signing secret, hex-encode it, prefix `v0=`, and compare against `X-Statsig-Signature`. Use the raw, unparsed body.

```javascript
const crypto = require("crypto");

const SIGNING_SECRET = process.env.STATSIG_SIGNING_SECRET;

function verify(rawBody, timestamp, signature) {
  const basestring = `v0:${timestamp}:${rawBody}`;
  const expected = "v0=" +
    crypto.createHmac("sha256", SIGNING_SECRET).update(basestring).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.headers["x-statsig-request-timestamp"];
  const signature = req.headers["x-statsig-signature"];
  if (!verify(req.body.toString("utf8"), timestamp, signature)) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  const payload = JSON.parse(req.body);
  const events = Array.isArray(payload) ? payload : payload.data || [];
  for (const event of events) processQueue.add(event); // process asynchronously
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

SIGNING_SECRET = os.environ["STATSIG_SIGNING_SECRET"].encode()

def verify(raw_body: str, timestamp: str, signature: str) -> bool:
    basestring = f"v0:{timestamp}:{raw_body}".encode()
    expected = "v0=" + hmac.new(SIGNING_SECRET, basestring, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

```

## Statsig webhook limitations and pain points

### The basestring and prefix are specific

The Problem: The signature is over `v0:{timestamp}:{body}` (colon-separated, with a `v0` version), and the header value is prefixed `v0=`. Signing only the body, or omitting the prefix, fails verification.

Why It Happens: Statsig uses a versioned Slack-style scheme rather than signing the body alone.

Workarounds:

* Build the exact `v0:{timestamp}:{raw body}` basestring and compare against the `v0=`-prefixed value.
* Use the raw body.

How Hookdeck Can Help: Hookdeck verifies the `X-Statsig-Signature` at the edge, so your app receives pre-verified events without reconstructing the basestring.

### Batched array payloads

The Problem: Payloads are JSON batches, and config changes are wrapped as `{"data": [...]}` while exposures may arrive as bare arrays. A handler that expects one event per request mishandles the batch.

Why It Happens: Statsig batches events for delivery efficiency and wraps some payload types.

Workarounds:

* Normalize to an array (handle both the bare array and the `{"data": [...]}` wrapper) and iterate.

How Hookdeck Can Help: Hookdeck can split batched payloads into individual events, so your app processes one clean event at a time.

### Undocumented retry behavior

The Problem: Statsig doesn't document its retry behavior, so you can't assume a failed delivery will be retried on a known schedule.

Why It Happens: The retry semantics simply aren't published.

Workarounds:

* Acknowledge reliably and persist events on receipt; reconcile against Statsig's console/API for critical audit data.

How Hookdeck Can Help: Hookdeck durably queues every delivery and retries to your downstream on a schedule you control, giving you retry guarantees Statsig doesn't document.

### No verification SDK

The Problem: There's no official SDK helper; the docs give HMAC pseudo-code, so you implement the check yourself.

Why It Happens: Statsig's SDKs target feature-flag evaluation, not webhook ingestion.

Workarounds:

* Use the constant-time HMAC verification above, centralized in one middleware.

How Hookdeck Can Help: Hookdeck verifies the signature at the edge, so your app receives pre-verified events without hand-rolled crypto.

## Best practices

### Verify the `v0:{timestamp}:{body}` basestring

Build the basestring, HMAC-SHA256 with your signing secret, hex-encode, prefix `v0=`, and compare in constant time against `X-Statsig-Signature`. Use the raw body.

### Handle batches and both payload shapes

Normalize bare arrays and `{"data": [...]}` wrappers to a list and iterate every event.

### Acknowledge fast, process asynchronously

Return 200 immediately and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Filter narrowly and persist

Use Event Filtering to receive only exposures or config changes you need, and persist events on receipt since retries aren't documented.

## Conclusion

Statsig Event Webhooks deliver config-change and exposure activity as signed JSON batches, verified with an `X-Statsig-Signature` over the `v0:{timestamp}:{body}` basestring. The details that matter are the exact basestring and `v0=` prefix, handling the batched (and sometimes `{"data": [...]}`-wrapped) payloads, and persisting events since retry behavior isn't documented.

[Hookdeck](https://hookdeck.com) verifies the signature, splits batches, and durably queues every event at the edge, so your app only ever processes verified events with delivery guarantees Statsig doesn't provide.

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