# Guide to Optimizely Webhooks: Features and Best Practices

Optimizely Feature Experimentation is Optimizely's product for feature flags, rollouts and A/B tests in server and app code. Its SDKs evaluate flags locally from a JSON datafile, so when someone changes a flag or rule, every running SDK needs the new datafile. Optimizely's project webhooks announce that change, and can also report other edits from the project's change history, such as audiences or rules being created or archived.

This guide covers Feature Experimentation webhooks only, not Web Experimentation or Optimizely's CMS products. It explains the datafile and change-history events, the one-time secret, and verifying the SHA-1 `X-Hub-Signature`.

## What are Optimizely webhooks?

Webhooks are configured per project under **Settings > Webhooks**. Each has a URL and a set of triggers: the datafile update, and any of the change-history entity and action pairs you tick.

When a trigger fires, Optimizely sends a JSON `POST` with the `project_id`, a Unix `timestamp`, the `event` name and a `data` payload. For a datafile update, `data` tells you the environment, the new revision and where to download it. For change-history events, `data` is an array of change records.

Requests are signed GitHub-style: `X-Hub-Signature: sha1=<hex>`, an HMAC-SHA1 of the raw body keyed with the webhook's secret token.

## Optimizely webhook features

| Feature | Details |
| --- | --- |
| Configuration | Project **Settings > Webhooks** |
| Triggers | Datafile updated; change-history entity x action pairs |
| Signature | hex HMAC-SHA1 |
| Signature header | `x-hub-signature` (`sha1=<hex>`) |
| Signed content | The raw request body |
| Key | Secret token generated by Optimizely, shown once |
| Body | JSON: `project_id`, `timestamp`, `event`, `data` |
| Event routing | Body field `event` |
| User-Agent | Google App Engine (`appid: s~optimizely-hrd`) |
| Retries and timeout | Not documented |

## Events and payloads

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `project.datafile_updated` | An environment's datafile is republished | Re-fetch the datafile from `data.cdn_url` so SDKs see new flags and rules |
| `project.ruleset_updated` | A flag ruleset changes (change history) | Audit logging, notifying a team channel |

The change-history events you can subscribe to are published only as a table of entities (Attribute, Audience, Flag, Rule and others) against actions (Create, Update, Archive, Delete), without the `event` names they produce. `project.ruleset_updated` suggests a `project.<entity>_<action>` pattern, but Optimizely doesn't confirm it, so log any `event` value you don't recognise.

### Event Payload Structure

For `project.datafile_updated`, `data` has the `revision`, `origin_url`, `cdn_url` and `environment`. For change-history events, `data` is an **array** of change records in the same shape as the "Retrieve changes for a project" REST response, each with a `change_type` and `entity.type`. Optimizely's change-history example is wrapped in an HTTP request description with placeholders, so check a real delivery for the exact fields.

### Published payload example

A datafile update for the Production environment, from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/optimizely/latest/project.datafile_updated.json).

```json
{
  "project_id": 1234,
  "timestamp": 1468447113,
  "event": "project.datafile_updated",
  "data": {
    "revision": 1,
    "origin_url": "https://optimizely.s3.amazonaws.com/json/1234.json",
    "cdn_url": "https://cdn.optimizely.com/json/1234.json",
    "environment": "Production"
  }
}

```

## Setting up Optimizely webhooks

### Prerequisites

* An Optimizely Feature Experimentation project and permission to manage its settings
* A publicly reachable HTTPS endpoint

### Get Your Secret Token

1. Open your project's **Settings > Webhooks** and choose **Create New Webhook**.
2. Enter your endpoint URL, for example `https://example.com/webhooks/optimizely`, and select the events.
3. Save. Optimizely generates a secret token and shows it **only once**, straight after creation. Copy it into `WEBHOOK_SECRET`.

If you lose the token, delete the webhook and create a new one; it can't be displayed again.

### Test a delivery

Start a local receiver and forward requests with the [Hookdeck CLI](/docs/cli):

```bash
npx hookdeck-cli listen 3000 optimizely --path /webhooks/optimizely

```

Create a webhook with the CLI's URL, then toggle a flag in a development environment. A datafile update should follow shortly after.

## Securing Optimizely webhooks

Strip the `sha1=` prefix from `X-Hub-Signature`, compute HMAC-SHA1 of the raw body with the secret token, hex-encode it, and compare in constant time. Optimizely's [webhook page](https://docs.optimizely.com/feature-experimentation/docs/configure-webhooks) publishes a test vector with a secret, a datafile body and the expected `sha1=` value, which is worth adding to your tests. The signed body contains spaces after `:` and `,`, so any re-serialisation breaks the match.

```javascript
const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-hub-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature.startsWith('sha1=') ? signature.slice(5) : signature];
  const expected = crypto.createHmac('sha1', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{40}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

The handler refreshes a cached datafile when the revision is newer than the one held for that environment, and records change-history events for auditing.

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

function createOptimizelyApp({ datafiles, auditLog }) {
  const app = express();
  app.post('/webhooks/optimizely', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
    let parsed;
    try {
      parsed = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
    const { project_id, timestamp, event, data } = parsed;
    if (typeof event !== 'string') return res.sendStatus(400);
    if (event === 'project.datafile_updated' && (!data || typeof data !== 'object')) {
      return res.sendStatus(400);
    }
    try {
      if (event === 'project.datafile_updated') {
        const current = await datafiles.revision(project_id, data.environment);
        if (current === undefined || data.revision > current) {
          await datafiles.refresh(project_id, data.environment, data.cdn_url, data.revision);
        }
      } else if (Array.isArray(data)) {
        await auditLog.append(project_id, event, timestamp, data);
      } else {
        console.log('Unhandled Optimizely event', event);
      }
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

```

The `timestamp` in the body isn't part of a documented freshness check, and only the body is signed. A replayed datafile notification is harmless with the revision check above, since an older revision is ignored.

Hookdeck's Optimizely source type reads `sha1=` as a label and compares the hex HMAC-SHA1 of the raw body. Optimizely always gets `200` back; a mismatch is stored as failed verification and isn't sent to your destination. There's no timestamp window.

> **Make Optimizely webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies each Optimizely webhook and fans datafile updates out to every service that runs an Optimizely SDK.

## Optimizely webhook limitations and pain points

### Secret shown once

**The Problem:** A lost or leaked secret can't be recovered or rotated in place.

**Why It Happens:** Optimizely displays the generated token only immediately after creating the webhook.

**Workarounds:**

* Save the token to a secret manager before leaving the page.
* To rotate, create a new webhook, switch your receiver, then delete the old one.

**How Hookdeck Can Help:** During a rotation, a second Hookdeck source can take the new webhook while the old one keeps working, so there's no gap.

### No retry or timeout documentation

**The Problem:** SDKs can keep serving an old datafile if a notification is missed.

**Why It Happens:** Optimizely doesn't document retries, timeouts or the response it expects.

**Workarounds:**

* Keep SDK datafile polling on as a fallback, at a longer interval.
* Return `200` quickly and fetch the datafile afterwards.

**How Hookdeck Can Help:** Hookdeck stores each notification and retries delivery to your service, so a brief outage doesn't drop the update.

### Unnamed change-history events

**The Problem:** You can't write a complete router for change-history events in advance.

**Why It Happens:** Optimizely lists entity and action pairs but not the `event` strings, and its one change-history example uses placeholders.

**Workarounds:**

* Route on whether `data` is an array, then on `entity.type` and `change_type` within each record.
* Collect real deliveries for each pair you enable.

**How Hookdeck Can Help:** Hookdeck's request log keeps the full body of every change-history delivery, giving you real examples to code against.

### SHA-1 signatures

**The Problem:** Security reviews may flag the use of SHA-1.

**Why It Happens:** Optimizely uses the legacy `X-Hub-Signature` scheme with HMAC-SHA1.

**Workarounds:**

* Note that HMAC-SHA1 remains a sound MAC even though SHA-1 collisions are practical, and document the decision.
* Keep the endpoint on HTTPS.

**How Hookdeck Can Help:** Hookdeck verifies the SHA-1 signature for you, and the connection from Hookdeck to your service can add its own authentication.

## Best practices

### Fetch from `cdn_url`, not the webhook

The datafile itself isn't in the payload. Download it from `data.cdn_url` and compare `revision` before replacing what you have.

### Keep environments separate

`data.environment` names the environment. Make sure a development datafile can never overwrite production in your cache.

### Process after acknowledging

Downloading a datafile and pushing it to many services takes time. Store the notification, return `200`, and do the work in the background as covered in the [asynchronous processing guide](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make audit records idempotent

With no delivery ID, use `project_id`, `event`, `timestamp` and the change records' identifiers as the key, following the [idempotency guide](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

For Feature Experimentation, the datafile webhook is the piece most teams need: it lets SDKs pick up flag changes quickly instead of waiting for a poll. Verification uses a GitHub-style SHA-1 header with a published test vector. The awkward parts are the show-once secret, unnamed change-history events and silence on retries.

[Hookdeck Event Gateway](https://hookdeck.com) verifies Optimizely's webhooks once and delivers datafile updates to every consumer that needs them.

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