Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

The availability of features may depend on your plan type. Contact your Customer Success Manager if you have any questions.

Dev guideRecipesAPI ReferenceChangelog
Dev guideAPI ReferenceRecipesChangelogUser GuideGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Create webhooks

How to create webhooks in Optimizely Feature Experimentation.

Webhooks in Optimizely Feature Experimentation can notify your server when certain events occur. This lets you send relevant and actionable user activity notifications to any external system.

Using a webhook also lets you notify your server when the datafile updates, eliminating the need to constantly poll and determine if there are changes in the Feature Experimentation configuration. When you receive the webhook and download the latest datafile, you must re-instantiate the Feature Experimentation client with the latest datafile for the changes to take effect.

Feature Experimentation sends a POST request to your supplied endpoint when you make a change in any environment.

📘

Note

When you change an environment, Feature Experimentation updates the revision of only that environment's datafile.

For example, if you have four environments and update one, Optimizely only creates a new datafile revision for that environment.

Available events

You can subscribe to notifications for any event captured in the Feature Experimentation change history.

Entity typeCreateUpdateArchiveDelete
AttributeYesYesYesYes
AudienceYesYesYesYes
DatafileNoYesNoNo
EnvironmentYesYesYesYes
EventYesYesYesYes
FlagYesYesYesYes
GroupYesYesYesYes
ProjectNoYesNoNo
RulesetYesYesNoYes
RuleYesYesNoYes
VariableYesYesNoYes
VariationYesYesYesYes

Create a webhook

Use the following steps to configure and test a webhook.

Configure an endpoint on your server

First, configure a public API endpoint on your server to accept a POST request from Feature Experimentation.

You can name the endpoint as desired, but for this example, you should name your endpoint /webhooks/optimizely.

Configuring a public endpoint on your server varies greatly depending on your language and framework. The following code examples show JavaScript (Node) using Express and Python using Flask:

// Simple Node Express webhook example )
// Requires installing express: 'npm install --save express'
const express = require('express');
const app = express();

/**
 * Optimizely Webhook Route
 * Route to accept webhook notifications from Optimizely
 **/
app.post('/webhooks/optimizely', (req, res) => {
  console.log(`
    [Optimizely] Optimizely webhook request received!
    The Optimizely datafile has been updated. Re-download
    the datafile and re-instantiate the Optimizely SDK
    for the changes to take effect
  `);
  res.send('Webhook Received');
});

app.get('/', (req, res) => res.send('Optimizely Webhook Example'))

const HOST = process.env.HOST || '0.0.0.0';
const PORT = process.env.PORT || 8080;
app.listen(PORT, HOST);
console.log(`Example App Running on http://${HOST}:${PORT}`);
# Simple Python Flask webhook example 
# Requires installing flask: 'pip install flask'
import os
from flask import Flask, request, abort

app = Flask(__name__)

# Route to accept webhook notifications from Optimizely
@app.route('/webhooks/optimizely', methods=['POST'])
def index():
  print("""
    [Optimizely] Optimizely webhook request received!
    The Optimizely datafile has been updated. Re-download
    the datafile and re-instantiate the Optimizely SDK
    for the changes to take effect
  """)
  return 'Webhook Received'

@app.route('/')
def hello_world():
  return 'Optimizely Webhook Example'

if __name__ == "__main__":
  host = os.getenv('HOST', '0.0.0.0')
  port = int(os.getenv('PORT', 3000))
  print('Example App unning on http://' + str(port) + ':' + str(host))
  app.run(host=host, port=port)

Create a webhook in Optimizely Feature Experimentation

After configuring an endpoint on your server, take note of the fully qualified URL.

If your server domain is https://www.your-example-site.com and you named your endpoint /webhooks/optimizely as described in the previous step, then your fully qualified webhook URL is https://www.your-example-site.com/webhooks/optimizely. Use this URL in the following steps:

  1. Go to Settings > Webhooks.

  2. Click Create New Webhook.

    create webhook
  3. Enter the URL where Feature Experimentation sends the datafile update notifications (for example, https://www.your-example-site.com/webhooks/optimizely).

  4. Select the events that trigger the webhook.

  5. Click Save.

  6. Take note of the secret generated to secure your webhook in the following step.

Example payloads

Datafile webhook example

The following example shows the webhook payload structure with the default Feature Experimentation primary environment of Production.

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

Change history webhook example

The following example shows the webhook payload structure for a running ruleset of the production environment for the flag product_details_page. Payloads sent by a webhook match the response schema of the Retrieve changes for a project REST API endpoint.

{
	"method": "post",
	"path": "/",
	"body": {
		"project_id": {project_id},
		"timestamp": 1719500691,
		"event": "project.ruleset_updated",
		"data": [
			{
				"change_type": "update",
				"project_id": {project_id},
				"user": {
					"display_name": "Optimizely Support",
					"id": "{[email protected]}",
					"email": "[email protected]"
				},
				"created": "2024-06-27T15:04:50.890414Z",
				"id": 41933132,
				"entity": {
					"type": "ruleset",
					"ui_url": "https://app.optimizely.com/v2/projects/{project_id}/flags/manage/product_detail_page/rules/production",
					"is_deleted": null,
					"id": 251219,
					"name": "Rules for product_detail_page in Production",
					"api_url": "https://api.optimizely.com/v2/projects/{project_id}/flags/product_detail_page/rules/production"
				},
				"changes": [
					{
						"after": "2024-06-27T15:04:50.803341Z",
						"before": "2024-06-27T15:04:41.247609Z",
						"description": "Updated rules.td.updated_time",
						"property": "rules.td.updated_time"
					},
					{
						"after": "2024-06-27T15:04:50.803341Z",
						"before": "2024-06-27T15:04:41.247609Z",
						"description": "Updated rules.mab.updated_time",
						"property": "rules.mab.updated_time"
					},
					{
						"after": "2024-06-27T15:04:50.803341Z",
						"before": "2024-06-27T15:04:41.247609Z",
						"description": "Updated rules.repeat_buyers_test.updated_time",
						"property": "rules.repeat_buyers_test.updated_time"
					},
					{
						"after": "running",
						"before": "paused",
						"description": "Updated status",
						"property": "status"
					},
					{
						"after": true,
						"before": false,
						"description": "Updated enabled",
						"property": "enabled"
					},
					{
						"after": 475,
						"before": 474,
						"description": "Updated revision",
						"property": "revision"
					},
					{
						"after": "2024-06-27T15:04:50.803341Z",
						"before": "2024-06-27T15:04:41.247609Z",
						"description": "Updated updated_time",
						"property": "updated_time"
					},
					{
						"after": "/projects/{project_id}/flags/product_detail_page/environments/production/ruleset/disabled",
						"description": "Added disable_url",
						"property": "disable_url"
					},
					{
						"before": "/projects/{project_id}/flags/product_detail_page/environments/production/ruleset/enabled",
						"description": "Removed enable_url",
						"property": "enable_url"
					}
				],
				"source": "ui",
				"summary": "Updated ruleset 'Rules for product_detail_page in Production'"
			}
		]
	}
}

🚧

Important

Your environment values may be different than the previous examples. See Manage environments for information.

Secure your webhook

After you configure a public endpoint that can accept datafile update notifications, you must secure your webhook to ensure that these notifications are coming from Feature Experimentation and not from some malicious user trying to control your server's feature flag configuration.

When you create a webhook, Feature Experimentation generates a secret token to create a hash signature of webhook payloads. Webhook requests include this signature in a header X-Hub-Signature that you can use to verify the request originated from Feature Experimentation.

You can only view a webhook's secret token once, immediately after its creation. If you forget a webhook's secret token, you must recreate it using the steps in the previous section.

📘

Note

Keep all webhook secret tokens for your implementation secure and private.

The X-Hub-Signature header contains a SHA1 HMAC hex digest of the webhook payload, using the webhook's secret token as the key and prefixed with sha1=. How you verify this signature varies, depending on the language of your codebase. The following reference implementation examples show JavaScript (Node) using Express and Python using Flask.

Both examples assume your webhook secret is passed as an environment variable named OPTIMIZELY_WEBHOOK_SECRET.

// Simple Node Express webhook 
// Requires installing express: 'npm install --save express body-parser'
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();

/**
 * Optimizely Webhook Route
 * Route to accept webhook notifications from Optimizely
 **/
app.post('/webhooks/optimizely', bodyParser.text({ type: '*/*' }), (req, res) => {
  const WEBHOOK_SECRET = process.env.OPTIMIZELY_WEBHOOK_SECRET;
  const webhookPayload = req.body;
  const hmac = crypto.createHmac('sha1', WEBHOOK_SECRET);
  const webhookDigest = hmac.update(webhookPayload).digest('hex');

  const computedSignature = Buffer.from(`sha1=${webhookDigest}`, 'utf-8');
  const requestSignature = Buffer.from(req.header('X-Hub-Signature', 'utf-8'));

  if (computedSignature.length != requestSignature.length || !crypto.timingSafeEqual(computedSignature, requestSignature)) {
    res.sendStatus(500);
    return;
  }

  console.log(`
    [Optimizely] Optimizely webhook request received!
    Signatures match! Webhook verified as coming from Optimizely
    Download Optimizely datafile and re-instantiate the SDK Client
    For the latest changes to take affect
  `);
  res.sendStatus(200);
});

app.get('/', (req, res) => res.send('Optimizely Webhook Example'));

const HOST = process.env.HOST || '0.0.0.0';
const PORT = process.env.PORT || 8080;
app.listen(PORT, HOST);
console.log(`Example App Running on http://${HOST}:${PORT}`);

# Reference Flask implementation of secure webhooks
# Requires installing flask: 'pip install flask'
# Assumes webhook's secret is stored in the environment variable OPTIMIZELY_WEBHOOK_SECRET
from hashlib import sha1
import hmac
import os

from flask import Flask, request, abort

app = Flask(__name__)

# Route to accept webhook notifications from Optimizely
@app.route('/webhooks/optimizely', methods=['POST'])
def index():
  request_signature = request.headers.get('X-Hub-Signature')
  webhook_secret = bytes(os.environ['OPTIMIZELY_WEBHOOK_SECRET'], 'utf-8')
  webhook_payload = request.data
  digest = hmac.new(webhook_secret, msg=webhook_payload, digestmod=sha1).hexdigest()
  computed_signature = 'sha1=' + str(digest)

  if not hmac.compare_digest(computed_signature, request_signature):
    print("[Optimizely] Signatures did not match! Do not trust webhook request")
    abort(500)
    return

  print("""
    [Optimizely] Optimizely webhook request received!
    Signatures match! Webhook verified as coming from Optimizely
    Download Optimizely datafile and re-instantiate the SDK Client
    For the latest changes to take affect
  """)
  return 'Secure Webhook Received'

@app.route('/')
def hello_world():
  return 'Optimizely Webhook Example'

if __name__ == "__main__":
  host = os.getenv('HOST', '0.0.0.0')
  port = int(os.getenv('PORT', 3000))
  print('Example App unning on http://' + str(port) + ':' + str(host))
  app.run(host=host, port=port)

❗️

Warning

To prevent timing analysis attacks, you should use a constant time string comparison function such as Python's hmac.compare_digest instead of the == operator when verifying webhook signatures.

Test your implementation

To ensure that your secure webhook implementation is correct, the following are example values that you can use to test your implementation.

Example webhook secret token

yIRFMTpsBcAKKRjJPCIykNo6EkNxJn_nq01-_r3S8i4

Example webhook request payload

'{"timestamp": 1558138293, "project_id": 11387641093, "data": {"cdn_url": "https://cdn.optimizely.com/datafiles/QMVJcUKEJZFg8pQ2jhAybK.json", "environment": "Production", "origin_url": "https://optimizely.s3.amazonaws.com/datafiles/QMVJcUKEJZFg8pQ2jhAybK.json", "revision": 13}, "event": "project.datafile_updated"}'

Using the webhook secret token and the webhook payload as a string, your code should generate a computed signature of sha1=b2493723c6ea6973fbda41573222c8ecb1c82666, which you can verify against the webhook request header.

Example webhook request header

X-Hub-Signature: sha1=b2493723c6ea6973fbda41573222c8ecb1c82666
Content-Type: application/json
User-Agent: AppEngine-Google; (+http://code.google.com/appengine; appid: s~optimizely-hrd)

📘

Note

If you use your secret instead of the example, your sha1 response differs.

Test your webhooks

Use a tool like Request Bin to test your webhooks. Request Bin lets you create a webhook URL to process HTTP requests and render the data in a human-readable format.

See the Request Bin documentation for information.