Gareth Wilson Gareth Wilson

Guide to Optimizely Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationProject Settings > Webhooks
TriggersDatafile updated; change-history entity x action pairs
Signaturehex HMAC-SHA1
Signature headerx-hub-signature (sha1=<hex>)
Signed contentThe raw request body
KeySecret token generated by Optimizely, shown once
BodyJSON: project_id, timestamp, event, data
Event routingBody field event
User-AgentGoogle App Engine (appid: s~optimizely-hrd)
Retries and timeoutNot documented

Events and payloads

EventTriggered WhenCommon Use Cases
project.datafile_updatedAn environment's datafile is republishedRe-fetch the datafile from data.cdn_url so SDKs see new flags and rules
project.ruleset_updatedA 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.

{
  "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:

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

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.

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

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.

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 verifies Optimizely's webhooks once and delivers datafile updates to every consumer that needs them.

Get started with Hookdeck for free and handle Optimizely 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.