Subscribe to events with webhooks
A webhook sends event data from CMS (SaaS) to a system of your choice when something happens to the content. Subscribe to the events your integration cares about. CMS (SaaS) then sends an HTTP POST request to your endpoint each time one of those events occurs, so your integration reacts to the event instead of polling CMS (SaaS) for changes.
Webhooks are useful when a change to content should trigger work somewhere else. Examples include sending content to Mark (Optimizely Agent Platform), pushing a published page to a translation service, and rebuilding a downstream cache when content is deleted or an import finishes.
Manage webhooks with the REST API, as described in this article, or from the CMS (SaaS) UI.
Prerequisites
- You need an access token to call the Webhooks endpoints.
- You need an endpoint that accepts HTTP POST requests and responds to the validation handshake that CMS (SaaS) sends to it. See Verify your endpoint.
Endpoints
Experimental The Webhooks endpoints are experimental and subject to change.
The Webhooks endpoints let you list the available event types and manage your subscriptions.
Endpoint | Method | Description |
|---|---|---|
/v1/experimental/webhookdefinitions | GET | Lists the event types you can subscribe to. |
/v1/experimental/webhooks | GET | Lists webhook subscriptions. |
/v1/experimental/webhooks | POST | Creates a webhook subscription. |
/v1/experimental/webhooks/{key} | GET | Gets a webhook subscription by its key. |
/v1/experimental/webhooks/{key} | PATCH | Updates a webhook subscription. |
/v1/experimental/webhooks/{key} | DELETE | Deletes a webhook subscription. |
Create a webhook subscription
Send a POST request with the subscription in the request body. The following example subscribes to two content events and filters them to a single content item.
HTTP headers
POST https://api.cms.optimizely.com/v1/experimental/webhooks
Content-Type: application/json
Authorization: Bearer ACCESS_TOKENBody
{
"key": "content-change-notifications",
"displayName": "Content change notifications",
"description": "Notifies the integration service when content is created or deleted.",
"endpoint": "https://example.com/hooks/cms",
"eventTypes": [
"content:preview1:created",
"content:preview1:deleted"
],
"dataFilters": [
{
"field": "key",
"operator": "StringIn",
"values": ["6946107a8ad6414f8f1786364dab1ec2"]
}
],
"bearerToken": "YOUR_SHARED_SECRET"
}CMS (SaaS) runs a validation handshake with the endpoint before it saves the subscription. When the handshake succeeds, CMS (SaaS) returns the stored subscription:
Response
{
"key": "content-change-notifications",
"displayName": "Content change notifications",
"description": "Notifies the integration service when content is created or deleted.",
"endpoint": "https://example.com/hooks/cms",
"eventTypes": [
"content:preview1:created",
"content:preview1:deleted"
],
"dataFilters": [
{
"field": "key",
"operator": "StringIn",
"values": ["6946107a8ad6414f8f1786364dab1ec2"]
}
],
"bearerToken": "Hidden",
"isVerified": true,
"isDisabled": false,
"created": "2026-08-26T10:30:00+00:00",
"createdBy": "USER_NAME",
"lastModified": "2026-08-26T10:30:00+00:00",
"lastModifiedBy": "USER_NAME"
}Each subscription covers events from one category and one version of that category. Do not mix event types from different categories in the same subscription.
Subscription fields
A subscription has the following fields.
Field | Description |
|---|---|
key | The unique identifier of the subscription. Three to 64 characters, and it can contain letters, numbers, and dashes. This is not the same as the content key in the event payloads. |
displayName | Required. The name that identifies the subscription in the UI. Up to 255 characters. |
description | Describes what the subscription is for. Up to 512 characters. |
endpoint | Required. The URL that receives the HTTP POST requests. |
eventTypes | The event types the subscription receives. See Event types. |
dataFilters | Narrows which events the subscription receives. See Data filters. |
bearerToken | A secret you choose. CMS (SaaS) sends it in the Authorization header of every request, so your endpoint can confirm the request came from CMS (SaaS). This is not the access token you use to call the Webhooks endpoints. Responses return Hidden in place of the secret. |
isVerified | Read-only. Whether the endpoint passed the validation handshake. |
isDisabled | Whether the subscription is turned off. When true, CMS (SaaS) does not send events to the endpoint. |
created, createdBy, lastModified, lastModifiedBy | Read-only audit fields. |
Update a webhook subscription
Send a PATCH request to change a subscription. The following example turns a subscription off without deleting it.
HTTP headers
PATCH https://api.cms.optimizely.com/v1/experimental/webhooks/content-change-notifications
Content-Type: application/merge-patch+json
Authorization: Bearer ACCESS_TOKENBody
{
"isDisabled": true
}Delete a webhook subscription
Send a DELETE request to remove a subscription. CMS (SaaS) stops sending events to its endpoint.
HTTP headers
DELETE https://api.cms.optimizely.com/v1/experimental/webhooks/content-change-notifications
Authorization: Bearer ACCESS_TOKENEvent types
An event type has the format {resource}:{version}:{action}, such as contentVersion:preview1:published. The resource is the category of entity the event applies to.
Call GET /v1/experimental/webhookdefinitions to list the event types your instance supports, along with their display names and descriptions.
The Filterable fields column lists the fields you can use in data filters for that event type.
Content
Events for a content item. The payload identifies the content item by its key.
Event type | Fires when | Payload fields | Filterable fields |
|---|---|---|---|
content:preview1:created | Content is created. | key | key |
content:preview1:updated | Content moves to a different parent, excluding a move to the wastebasket. | key | key |
content:preview1:deleted | Content is deleted, either moved to the wastebasket or deleted permanently. | key, isPermanent | key, isPermanent |
The content payload has the following schema and sample.
Schema
{
"type": "object",
"properties": {
"key": { "type": "string" },
"isPermanent": { "type": "boolean" }
},
"required": ["key"]
}Sample
{
"key": "6946107a8ad6414f8f1786364dab1ec2",
"isPermanent": false
}The isPermanent field only appears on content:preview1:deleted. It is true when the content is deleted permanently, and false when the content moves to the wastebasket.
Deleting a content item together with its children sends one content:preview1:deleted event, not one event for each child.
Content that an import creates does not send content:preview1:created. See Import.
Content version
Events for a specific version of a content item. The payload identifies the version by the content key and the version.
Event type | Fires when | Payload fields | Filterable fields |
|---|---|---|---|
contentVersion:preview1:published | Content is published. | key, version | key, version |
contentVersion:preview1:scheduled | Content is scheduled for publishing at a future date. | key, version | key, version |
contentVersion:preview1:localeDeleted | A language branch of content is deleted. | key, version | key, version |
The content version payload has the following schema and sample.
Schema
{
"type": "object",
"properties": {
"key": { "type": "string" },
"version": { "type": "string" }
},
"required": ["key", "version"]
}Sample
{
"key": "6946107a8ad6414f8f1786364dab1ec2",
"version": "456"
}Content that an import publishes or schedules does not send contentVersion:preview1:published or contentVersion:preview1:scheduled. See Import.
Display template
Events for a display template. The payload is the display template resource.
Event type | Fires when | Filterable fields |
|---|---|---|
displayTemplate:v1:created | A display template is created. | id, key, name, nodeType, baseType, contentTypeID, isDefault |
displayTemplate:v1:updated | An existing display template is updated. | id, key, name, nodeType, baseType, contentTypeID, isDefault |
displayTemplate:v1:deleted | An existing display template is deleted. | id, key, name, nodeType, baseType, contentTypeID, isDefault |
Webhook
Events for the webhook subscriptions themselves, so you can track configuration changes. The payload is the subscription resource described in Subscription fields.
Event type | Fires when | Filterable fields |
|---|---|---|
webhookSubscription:v1:created | A webhook subscription is created. | key, displayName, description, endpoint, isVerified, isDisabled |
webhookSubscription:v1:updated | An existing webhook subscription is updated. | key, displayName, description, endpoint, isVerified, isDisabled |
webhookSubscription:v1:deleted | An existing webhook subscription is deleted. | key, displayName, description, endpoint, isVerified, isDisabled |
Import
Events for a content import. CMS (SaaS) sends one event when an Episerver Data package import finishes, whether the import runs through the REST API or Settings > Import Data. Subscribe to it to start follow-up work, such as warming a cache, after all the imported content is in place. The import does not send content events for each item it creates, publishes, or schedules.
Event type | Fires when | Payload fields | Filterable fields |
|---|---|---|---|
import:preview1:completed | A content import finishes. | counts, hasErrors | None |
The import payload has the following schema and sample.
Schema
{
"type": "object",
"properties": {
"counts": {
"type": "object",
"properties": {
"contentItems": { "type": "integer" },
"contentTypes": { "type": "integer" },
"blueprints": { "type": "integer" },
"categories": { "type": "integer" },
"tabDefinitions": { "type": "integer" },
"displayTemplates": { "type": "integer" },
"contentBindingDefinitions": { "type": "integer" }
}
},
"hasErrors": { "type": "boolean" }
},
"required": ["counts", "hasErrors"]
}Sample
{
"counts": {
"contentItems": 120,
"contentTypes": 8,
"blueprints": 2,
"categories": 0,
"tabDefinitions": 3,
"displayTemplates": 1,
"contentBindingDefinitions": 0
},
"hasErrors": false
}- counts – The number of items the import brought in for each entity type. tabDefinitions counts property groups.
- hasErrors – true when the import finished but logged one or more errors.
CMS (SaaS) does not send the event for a test import that only verifies the file, or for an import that is aborted. If your integration waits for this event after it starts an import, set a timeout.
Data filters
A data filter narrows which events reach your endpoint by matching a field in the event payload. A subscription only receives an event when the event matches the filters.
Each filter has the following fields.
Field | Description |
|---|---|
field | The payload field to match, such as key. Use the Filterable fields listed for the event type in Event types. The field name cannot contain spaces, start or end with a period, or contain consecutive periods. |
operator | The comparison to apply. See Filter operators. |
values | The values to compare the field against. Range operators take each range as two numbers separated by a comma, such as "1,10". |
The following limits apply to filters:
- A maximum of 20 values across the filters on a subscription.
- A maximum of 475 characters for each value.
Filter operators
The following operators are available.
Operator | Matches when the field | Values |
|---|---|---|
StringIn | Equals one of the values. | One or more strings |
StringNotIn | Equals none of the values. | One or more strings |
StringContains | Contains one of the values. | One or more strings |
StringNotContains | Contains none of the values. | One or more strings |
StringBeginsWith | Begins with one of the values. | One or more strings |
StringNotBeginsWith | Begins with none of the values. | One or more strings |
StringEndsWith | Ends with one of the values. | One or more strings |
StringNotEndsWith | Ends with none of the values. | One or more strings |
NumberIn | Equals one of the numbers. | One or more numbers |
NumberNotIn | Equals none of the numbers. | One or more numbers |
NumberInRange | Falls within one of the ranges. | One or more ranges |
NumberNotInRange | Falls outside every range. | One or more ranges |
NumberLessThan | Is less than the number. | One number |
NumberLessThanOrEquals | Is less than or equal to the number. | One number |
NumberGreaterThan | Is greater than the number. | One number |
NumberGreaterThanOrEquals | Is greater than or equal to the number. | One number |
BoolEquals | Equals the boolean value. | true or false |
IsNullOrUndefined | Has no value. | None |
IsNotNull | Has a value. | None |
Callback delivery
CMS (SaaS) delivers each event to your endpoint as a CloudEvent, which is a standard envelope format for event data. The type field holds the event type. The subject field holds the path of the affected entity, in the form /{resource}/{version}/{key}. For contentVersion events, the subject ends with the version identifier when the event has one. Import events do not apply to a single entity, so their subject is /import/preview1. The entity itself is in the data field.
The following example shows the body of a delivered event.
Example request body
{
"type": "contentVersion:preview1:published",
"subject": "/contentVersion/preview1/6946107a8ad6414f8f1786364dab1ec2/456",
"data": {
"key": "6946107a8ad6414f8f1786364dab1ec2",
"version": "456"
}
}CMS (SaaS) sends an event only when the subscription is verified, is not disabled, and matches both the event type and any data filters.
Optimizely provisions and manages the delivery infrastructure. This includes the retries for failed deliveries and the dead-letter queue for events that cannot be delivered. These settings are not configurable. Because a failed delivery is retried, your endpoint can receive the same event more than once. Handle events idempotently, so that processing the same event twice has the same result as processing it one time.
Authenticate the requests
When the subscription sets bearerToken, CMS (SaaS) sends that value in the Authorization header of every request:
Request header
Authorization: Bearer YOUR_SHARED_SECRETConfirm the token before you act on the request. This token is the static secret stored on the subscription, not the access token you use to call the Webhooks endpoints.
Verify your endpoint
CMS (SaaS) confirms that your endpoint agrees to receive events before it saves a subscription. When you create a subscription, or change the endpoint URL of an existing one, CMS (SaaS) runs a validation handshake with the endpoint. The handshake follows the CloudEvents 1.0 webhook specification. Your endpoint must answer an HTTP OPTIONS request with status code 200 and a WebHook-Allowed-Origin header.
If the handshake fails, CMS (SaaS) returns status code 400 with a message that the endpoint URL could not be validated, and does not save the subscription. If the request changed the endpoint URL of an existing subscription, the subscription keeps its previous URL. A saved subscription therefore has isVerified set to true.
For the full list of response headers and example handlers in Node.js and C#, see Verify the endpoint.