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>_ 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
- Open your project's Settings > Webhooks and choose Create New Webhook.
- Enter your endpoint URL, for example
https://example.com/webhooks/optimizely, and select the events. - 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
200quickly 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
datais an array, then onentity.typeandchange_typewithin 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.