Automate Sitecore XM Cloud Webhook Registration with Azure DevOps Pipeline & PowerShell

Amit KumarAmit Kumar
23 min read

🚀 Automate Azure Webhook Registration for Sitecore XM Cloud: A CI/CD Approach

In today's fast-moving digital world, keeping content in sync in real-time is key to providing dynamic and engaging user experiences. Sitecore XM Cloud, with its Experience Edge platform, offers strong tools for managing and sharing content. As mentioned before, using Azure Functions and webhooks with XM Cloud is important for starting actions when content is updated.

My previous blog, Azure Function Webhook XM Cloud Integration, covered the basics of generating client credentials, acquiring access tokens, registering webhooks with Experience Edge, and understanding its execution modes, along with key considerations when working with XM Cloud versus Experience Edge webhooks. In earlier blogs, we discussed integrating Azure Functions with Sitecore XM Cloud using webhooks to achieve real-time content sync between XM Cloud and Sitecore Search. However, manually setting up and managing these webhooks, particularly generating tokens and registering them repeatedly, can be time-consuming and error-prone.

In this article, we’ll walk you through how to automate the registration of a custom Azure webhook with Sitecore XM Cloud using an Azure DevOps pipeline and a custom PowerShell script, improving deployment efficiency across environments. 🔝

🔧 Why Automate XM Cloud Webhook Registration?

Manually registering webhooks involves:
✔ Generating client credentials
✔ Fetching JWT access tokens (expiring in 24 hours)
Repeat the same steps for different environments
✔ Calling the Experience Edge Admin API

This process can cause delays, missed updates, and wasted developer hours, especially when working across development, staging, and production environments. 🔝

Automation ensures:
No manual token refreshes
Reusable across environments
Seamless pipeline integration
Reduced human error and faster deployment cycles

⚙️ Solution: Automation Process

In this blog post, I will explain how to automate the process using two methods: one with a PowerShell script and the other with PowerShell and Azure DevOps to register a custom Azure webhook with Sitecore XM Cloud. 🔝

Option 1: Automate Using PowerShell

In this approach, I will show you how to use a PowerShell script along with the .env (environment) file to execute the necessary steps from your LOCAL or DEV environment. Alternatively, you can replace the .env file environment variables with your preferred secret manager or configuration management tool.

Step 1: Set Up Environment Variables in a .env File

To effectively organize essential environment variables in a .env file, ensure you have all necessary values ready, such as client credentials and access tokens. These prerequisites are crucial for initiating the automation process to register the XM Cloud webhook, streamlining your deployment workflow. 🔝

BEARER_TOKEN=<If available>
CLIENT_ID=<Your Client ID, check https://enlightenwithamit.hashnode.dev/azure-function-webhook-xm-cloud-integration for more details>
CLIENT_SECRET=<Your Client Secret, check https://enlightenwithamit.hashnode.dev/azure-function-webhook-xm-cloud-integration for more details>
AUDIENCE=https://api.sitecorecloud.io
GRANT_TYPE=client_credentials
SITECORE_EDGE_API=https://edge.sitecorecloud.io/api/admin/v1/webhooks
SITECORE_AUTH_API=https://auth.sitecorecloud.io/oauth/token

Step 2: How to Read Environment Variables from a .env File

To read environment variables from a .env file in a PowerShell script, I wrote a custom PowerShell function that gives you the value of an environment variable in the script. This method lets you manage settings separately from your main code, making it more modular and flexible. By using environment variables, you can easily change settings between different environments (like development, staging, production) without changing the code. This also helps keep the code cleaner and reduces the risk of exposing sensitive information.

I made a custom GetEnvVariable function that takes the path of the .env file and the name of the environment variable you want the value for. 🔝

function GetEnvVariable {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the path of the .env file.")]
        [string]$filePath = ".env",  # Default path for the .env file
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the .env name.")]
        [string]$varName  # Name of the environment variable to retrieve
    )

    Write-Host "Inside GetEnvVariable"  # Log entry into the function

    $envFilePath = Resolve-Path "$PSScriptRoot\$filePath"  # Resolve the full path of the .env file

    if (Test-Path $envFilePath) {  # Check if the .env file exists
        if ($varName -ne "" -and $envFilePath -ne "") {  # Ensure parameters are not empty
            Write-Host "Using .env file: $envFilePath" -ForegroundColor Cyan  # Log the file being used
            # Read the contents of the .env file
            $envFileContent = Get-Content -Path $envFilePath  # Load the file content into an array

            # Iterate through each line to find the variable
            foreach ($line in $envFileContent) {
                if ($line -match "^\s*$varName\s*=\s*(.+)\s*$") {  # Check if the line matches the variable name
                    $varValue = $matches[1].Trim()  # Extract the variable value
                    Write-Host "Environment variable '$varName' found with value '$varValue'."  # Log success message
                    return $varValue  # Return the found value
                }
            }

            Write-Host "Environment variable '$varName' not found in the .env file." -ForegroundColor Yellow  # Log if not found
            return $null  # Return null if not found
        } else {
            Write-Host "Invalid parameters" -ForegroundColor Red  # Log error for invalid parameters
            return $null  # Return null for invalid parameters
        }
    } else {
        Write-Error "The .env file does not exist at the specified path: $envFilePath"  # Log error if file doesn't exist
        return $null  # Return null if file doesn't exist
    }
}

Using the syntax below, you can call this function:

GetEnvVariable -filePath $EnvFileName -varName "BEARER_TOKEN"

Step 3: Requesting a JWT for Sitecore XM Cloud APIs

Automating the process of requesting a JWT for the Authoring and Management API or the XM Cloud Deploy API is crucial for maintaining seamless operations, especially when tokens expire or haven't been generated yet. A custom PowerShell script function GetAccessToken can handle JWT generation automatically, ensuring uninterrupted access. Before requesting a JWT, it's essential to create client credentials, including a client ID and client secret, tailored to the access level required. For detailed steps on setting up these credentials, refer to my previous blog on Azure Function Webhook XM Cloud Integration. This approach not only enhances security but also streamlines the authentication process across different environments. 🔝

function GetAccessToken {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the path of the .env file.")]
        [string]$filePath = ".env"  # Default path for the .env file
    )

    Write-Host "Inside GetAccessToken"  # Log entry into the function
    $statusCodeAccessToken = 0  # Initialize status code
    $envFilePath = Resolve-Path "$PSScriptRoot\$filePath"  # Resolve the full path of the .env file

    if (Test-Path $envFilePath) {  # Check if the .env file exists
        Write-Host "Using .env file: $envFilePath" -ForegroundColor Cyan  # Log the file being used
        # Read the contents of the .env file
        $audience = GetEnvVariable -filePath $EnvFileName -varName "AUDIENCE"
        $grantType = GetEnvVariable -filePath $EnvFileName -varName "GRANT_TYPE"
        $clientId = GetEnvVariable -filePath $EnvFileName -varName "CLIENT_ID"
        $clientSecret = GetEnvVariable -filePath $EnvFileName -varName "CLIENT_SECRET"
        $sitecoreAuthApi = GetEnvVariable -filePath $EnvFileName -varName "SITECORE_AUTH_API"

        $authBody = @{
            audience = $audience
            grant_type = $grantType
            client_id = $clientId
            client_secret = $clientSecret
        } | ConvertTo-Json  # Prepare the body for the authentication request

        try {

            # Make the API call
            $response = Invoke-WebRequest -Uri $sitecoreAuthApi -Method POST -Body $authBody -ContentType "application/json" -ErrorAction Stop

            # Extract the status code
            $statusCodeAccessToken = $response.StatusCode

            # Parse the response content
            $authResponse = $response.Content | ConvertFrom-Json


            # Debug logs
            # Write-Host "Status Code Access Token: $statusCodeAccessToken"
            # Write-Host "Response Content GetAccessToken:"
            # Write-Host $authResponse

        } catch {
            Write-Host "Error occurred while getting new access token: $($_.Exception.Message)" -ForegroundColor Red
            if ($_.Exception.Response -ne $null) {
                $statusCodeAccessToken = $_.Exception.Response.StatusCode
                $errorContent = $_.Exception.Response.GetResponseStream() | %{ $_.ReadToEnd() }
                Write-Host "Error Response Content:"
                Write-Host $errorContent
            } else {
                Write-Host "No response received from the server." -ForegroundColor Yellow
            }
            throw "Unexpected error occurred while getting new access token: $($_.Exception.Message)"
        }

        # Handle the status code and execute the next step
        switch ($statusCodeAccessToken) {
            200 {
                if ($authResponse -and $authResponse.access_token) {
                    $bearerToken = $authResponse.access_token
                    Write-Host "Successfully retrieved new access token with Status Code 200."  # Log success message
                    Write-Host "Access Token Retrieved: $bearerToken"
                } else {
                    Write-Error  "Status Code 200 - Access token not found in the response."
                }                
                return $bearerToken  # Return the new access token
            }
            "OK" {

                if ($authResponse -and $authResponse.access_token) {
                    $bearerToken = $authResponse.access_token
                    Write-Host "Successfully retrieved new access token with Status Code 200 OK"  # Log success message
                    # Write-Host "Access Token Retrieved: $bearerToken"
                } else {
                    Write-Error "Status Code OK - Access token not found in the response."
                } 

                return $bearerToken  # Return the new access token
            }
            default {
                if ($statusCodeAccessToken -match "OK") {

                    if ($authResponse -and $authResponse.access_token) {
                        $bearerToken = $authResponse.access_token
                        Write-Host "Successfully retrieved new access token with Status Code Default OK IF block"  # Log success message
                        # Write-Host "Access Token Retrieved: $bearerToken"
                    } else {
                        Write-Error "Status Code Default ELSE - Access token not found in the response."
                    }                     

                    return $bearerToken  # Return the new access token
                    } else {
                        Write-Error "Failed to retrieve new access token in Default - ELSE switch. Status code: $statusCodeAccessToken"  # Log error if not successful
                    # Add your next step for other errors here
                }
            }
        }


        if (-not $authResponse.access_token) {
            Write-Error "Failed to retrieve new access token."
            exit 1
        }
    } else {
        Write-Error "The .env file does not exist at the specified path: $envFilePath"  # Log error if file doesn't exist
        return $null  # Return null if file doesn't exist
    }
}

Using the syntax below, you can call this function:

$bearerToken=GetAccessToken -filePath $EnvFileName

You need to give the path to your .env file, and it will return the new JWT token for you to use. 🔝

Step 4: Registering Webhooks with Sitecore Experience Edge

A custom script makes it easy to register webhooks with Sitecore Experience Edge by automating the needed API calls. This reduces manual work and mistakes. The script takes care of authentication, gets access tokens, and registers the webhook, making sure it works smoothly with Sitecore's content delivery network. By using this automation, you can quickly set up webhooks to trigger actions like ISR revalidation when content is published, improving the responsiveness of your Sitecore XM Cloud apps. This method saves time and ensures consistent and reliable webhook management across different environments.

I created a custom RegisterWebhook function that uses the following parameters: 🔝

filePath: Specifies the path of the .env file.
webHookName: Specifies the webhook name.
webHookURL: Specifies the webhook URL.
createdBy: Specifies the Created By Name.
functionKey: Specifies the security key for your webhook.

function RegisterWebhook {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the path of the .env file.")]
        [string]$filePath = ".env" , # Default path for the .env file
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the webhook name.")]
        [string]$webHookName,  # Webhook Name to register
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the webhook url.")]
        [string]$webHookURL,  # URL to register
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the Created By Name.")]
        [string]$createdBy,  # Registration done by
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the Security Key for your Webhook.")]
        [string]$functionKey  # Security key for the webhook
    )

    Write-Host "Inside RegisterWebhook"  # Log entry into the function

    $envFilePath = Resolve-Path "$PSScriptRoot\$filePath"  # Resolve the full path of the .env file

    if (Test-Path $envFilePath) {  # Check if the .env file exists
        if ($varName -ne "" -and $envFilePath -ne "") {  # Ensure parameters are not empty
            Write-Host "Using .env file: $envFilePath" -ForegroundColor Cyan  # Log the file being used

            # Read the contents of the .env file
            # Define the API endpoint
            $sitecoreEdgeApi = GetEnvVariable -filePath $EnvFileName -varName "SITECORE_EDGE_API"

            # Write-Host "Local variable bearerToken set with value '$bearerToken'."

            Write-Host "Calling Sitecore Edge API to register webhooks..."

            # Prepare the headers
            $headers = @{ Authorization = "Bearer $bearerToken" }

            # Prepare the body
            $body = @{
                label = $webHookName
                uri = $webHookURL
                method = "POST"
                headers = @{
                    "x-functions-key" = $functionKey
                }
                createdBy = $createdBy
                executionMode = "OnUpdate"
            } | ConvertTo-Json -Depth 10  # Convert the body to JSON            

            Write-Host "Calling Sitecore Edge API to register webhook..."
            Write-Host "Request Body: $body"

            try {
                $response = Invoke-WebRequest -Uri $sitecoreEdgeApi -Headers $headers -Method POST -Body $body -ContentType "application/json" -ErrorAction Stop
                $statusCode = $response.StatusCode

                # Print the raw response content
                # Write-Host "Response Content GetWebhookListing:"
                # Write-Host $response.Content                
            } catch {
                Write-Host "Error occurred in RegisterWebhook: $($_.Exception.Message)" -ForegroundColor Red

                if ($_.Exception.Response -ne $null) {
                    $statusCode = $_.Exception.Response.StatusCode
                } else {
                    throw "Unexpected error occurred in RegisterWebhook: $($_.Exception.Message)"
                }
            }

            # Handle the status code and execute the next step
            switch ($statusCode) {
                201 {
                    Write-Host "Webhook registered successfully. Response:"
                    Write-Host $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 10
                }
                401 {
                    Write-Host "Unauthorized access 401. Please check your bearer token."
                    Write-Host "Response content: $($response.Content)"
                    Write-Warning "JWT verification failed. Requesting new token..."
                    # Add your next step for unauthorized access here
                    $bearerToken=GetAccessToken -filePath $EnvFileName
                    Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                    RegisterWebhook -filePath $filePath -webHookName $webHookName -webHookURL $webHookURL -createdBy $createdBy -functionKey $functionKey
                }
                default {
                    if ($statusCode -match "Unauthorized") {
                        Write-Host "Unauthorized error detected. Please verify your credentials or token."
                        Write-Warning "JWT verification failed. Requesting new token..."
                        # Add your next step for handling unauthorized errors here
                        $bearerToken=GetAccessToken -filePath $EnvFileName
                        Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                        RegisterWebhook -filePath $filePath -webHookName $webHookName -webHookURL $webHookURL -createdBy $createdBy -functionKey $functionKey
                    } else {
                        Write-Host "Failed to register webhook. Status code: $statusCode"
                        Write-Host "Response content: $($response.Content)"
                        # Add your next step for other errors here
                    }
                }
            }

            return $null  # Return null if not found
        } 
    } else {
        Write-Error "The .env file does not exist at the specified path: $envFilePath"  # Log error if file doesn't exist
        return $null  # Return null if file doesn't exist
    }
}

You can call this function using the syntax below: 🔝

RegisterWebhook -filePath $EnvFileName -webHookName "Amit Kumar GetUpdate" -webHookURL "https://amitkumar.com/GetUpdate" -createdBy "Amit Kumar" -functionKey "12345"
💡
Why use the function keys with your webhooks? ➡️ Function keys are used with webhooks to improve security and control access. They serve as a secret token that verifies requests to your webhook, making sure only authorized sources can activate it. This helps prevent unauthorized access and potential misuse, keeping your application's interactions secure and intact.

The RegisterWebhook function also provides useful information through logs, allowing you to understand what is happening during the process. It also returns the registered webhook details, which can be helpful for debugging issues. You can enhance the logging according to your needs. Some of the log details include: 🔝

Inside RegisterWebhook
Using .env file: C:\.env
Inside GetEnvVariable
Using .env file: C:\.env
Environment variable 'SITECORE_EDGE_API' found with value 'https://edge.sitecorecloud.io/api/admin/v1/webhooks'.
Calling Sitecore Edge API to register webhooks...
Request Body: {
    "label":  "Amit Kumar GetUpdate",
    "createdBy":  "Amit Kumar",
    "executionMode":  "OnUpdate",
    "method":  "POST",
    "uri":  "https://amitkumar.com/GetUpdate",
    "headers":  {
                    "x-functions-key":  "12345"
                }
}
Webhook registered successfully. Response:
{"id":"57614d29-e3e2-43f2-8d8e-04041eac1151","tenantId":"amitkumar-xmc123-amitiukumar","label":"Amit Kumar GetUpdate","uri":"https://amitkumar.com/GetUpdate","method":"POST","headers":{"x-functions-key":"12345"},"body":"","createdBy":"Amit Kumar","created":"2025-01-15T11:15:50.7956347+00:00","bodyInclude":null,"executionMode":"OnUpdate","lastRuns":[],"disabled":null}

The switch statement in the RegisterWebhook function is designed to handle various HTTP response status codes effectively, ensuring seamless webhook registration. When the response status code is 401 (Unauthorized), it indicates that the existing JWT token has expired or is invalid. In this case, the script intelligently calls the GetAccessToken function to fetch a new bearer token dynamically. Once the new token is retrieved, the RegisterWebhook function is invoked again with the updated token to retry the webhook registration process. This robust error-handling mechanism ensures uninterrupted API communication and automates token renewal for secure and efficient webhook management. 🔝

switch ($statusCode) {
                201 {
                    Write-Host "Webhook registered successfully. Response:"
                    Write-Host $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 10
                }
                401 {
                    Write-Host "Unauthorized access 401. Please check your bearer token."
                    Write-Host "Response content: $($response.Content)"
                    Write-Warning "JWT verification failed. Requesting new token..."
                    # Add your next step for unauthorized access here
                    $bearerToken=GetAccessToken -filePath $EnvFileName
                    Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                    RegisterWebhook -filePath $filePath -webHookName $webHookName -webHookURL $webHookURL -createdBy $createdBy -functionKey $functionKey
                }
                default {
                    if ($statusCode -match "Unauthorized") {
                        Write-Host "Unauthorized error detected. Please verify your credentials or token."
                        Write-Warning "JWT verification failed. Requesting new token..."
                        # Add your next step for handling unauthorized errors here
                        $bearerToken=GetAccessToken -filePath $EnvFileName
                        Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                        RegisterWebhook -filePath $filePath -webHookName $webHookName -webHookURL $webHookURL -createdBy $createdBy -functionKey $functionKey
                    } else {
                        Write-Host "Failed to register webhook. Status code: $statusCode"
                        Write-Host "Response content: $($response.Content)"
                        # Add your next step for other errors here
                    }
                }
            }

Option 2: Automate Using Azure Pipeline + PowerShell

This option shows how to automate Azure webhook registration for Sitecore XM Cloud using a PowerShell script and Azure Pipelines. It makes it easier to generate access tokens, set up webhooks, and connect with the Experience Edge API for better content management. 🔝

Step 1: Set Up Environment Variables in Azure Pipelines

To securely manage sensitive data, store the following in your Azure Pipelines environment variables:

BEARER_TOKEN=<If available>
CLIENT_ID=<Your Client ID, check https://enlightenwithamit.hashnode.dev/azure-function-webhook-xm-cloud-integration for more details>
CLIENT_SECRET=<Your Client Secret, check https://enlightenwithamit.hashnode.dev/azure-function-webhook-xm-cloud-integration for more details>
AUDIENCE=https://api.sitecorecloud.io
GRANT_TYPE=client_credentials
SITECORE_EDGE_API=https://edge.sitecorecloud.io/api/admin/v1/webhooks
SITECORE_AUTH_API=https://auth.sitecorecloud.io/oauth/token

Step 2: PowerShell Script to Handle Token Generation

The PowerShell script GetAccessToken performs requests a new JWT access token using the client credentials flow if the current token is expired or missing.

This script differs from Option 1 because we are not using the .env file to get the environment variables. Instead, we will retrieve the necessary environment variables from the Azure Pipelines Environment Variables. 🔝

function GetAccessToken {

    Write-Host "Inside GetAccessToken"  # Log entry into the function
    $statusCodeAccessToken = 0  # Initialize status code

      # Check if the .env file exists
        Write-Host "Read Environment Variables" -ForegroundColor Cyan  # Log the file being used
        # Read the contents of the .env file
        $audience = ${env:AUDIENCE}
        $grantType = ${env:GRANT_TYPE}
        $clientId = ${env:CLIENT_ID}
        $clientSecret = ${env:CLIENT_SECRET}
        $sitecoreAuthApi = ${env:SITECORE_AUTH_API}

        $authBody = @{
            audience = $audience
            grant_type = $grantType
            client_id = $clientId
            client_secret = $clientSecret
        } | ConvertTo-Json  # Prepare the body for the authentication request

        try {

            # Make the API call
            $response = Invoke-WebRequest -Uri $sitecoreAuthApi -Method POST -Body $authBody -ContentType "application/json" -ErrorAction Stop

            # Extract the status code
            $statusCodeAccessToken = $response.StatusCode

            # Parse the response content
            $authResponse = $response.Content | ConvertFrom-Json


            # Debug logs
            # Write-Host "Status Code Access Token: $statusCodeAccessToken"
            # Write-Host "Response Content GetAccessToken:"
            # Write-Host $authResponse

        } catch {
            Write-Host "Error occurred while getting new access token: $($_.Exception.Message)" -ForegroundColor Red
            if ($_.Exception.Response -ne $null) {
                $statusCodeAccessToken = $_.Exception.Response.StatusCode
                $errorContent = $_.Exception.Response.GetResponseStream() | %{ $_.ReadToEnd() }
                Write-Host "Error Response Content:"
                Write-Host $errorContent
            } else {
                Write-Host "No response received from the server." -ForegroundColor Yellow
            }
            throw "Unexpected error occurred while getting new access token: $($_.Exception.Message)"
        }

        # Handle the status code and execute the next step
        switch ($statusCodeAccessToken) {
            200 {
                if ($authResponse -and $authResponse.access_token) {
                    $bearerToken = $authResponse.access_token
                    Write-Host "Successfully retrieved new access token with Status Code 200."  # Log success message
                    # Write-Host "Access Token Retrieved: $bearerToken"
                } else {
                    Write-Error  "Status Code 200 - Access token not found in the response."
                }                
                return $bearerToken  # Return the new access token
            }
            "OK" {

                if ($authResponse -and $authResponse.access_token) {
                    $bearerToken = $authResponse.access_token
                    Write-Host "Successfully retrieved new access token with Status Code 200 OK"  # Log success message
                    # Write-Host "Access Token Retrieved: $bearerToken"
                } else {
                    Write-Error "Status Code OK - Access token not found in the response."
                } 

                return $bearerToken  # Return the new access token
            }
            default {
                if ($statusCodeAccessToken -match "OK") {

                    if ($authResponse -and $authResponse.access_token) {
                        $bearerToken = $authResponse.access_token
                        Write-Host "Successfully retrieved new access token with Status Code Default OK IF block"  # Log success message
                        # Write-Host "Access Token Retrieved: $bearerToken"
                    } else {
                        Write-Error "Status Code Default ELSE - Access token not found in the response."
                    }                     

                    return $bearerToken  # Return the new access token
                    } else {
                        Write-Error "Failed to retrieve new access token in Default - ELSE switch. Status code: $statusCodeAccessToken"  # Log error if not successful
                    # Add your next step for other errors here
                }
            }
        }


        if (-not $authResponse.access_token) {
            Write-Error "Failed to retrieve new access token."
            exit 1
        }

}

Step 3: Register Webhook via Experience Edge API

This PowerShell script function GetAccessToken uses the JWT token to call the Experience Edge admin API (https://edge.sitecorecloud.io/api/admin/v1/webhooks) to register webhook or update webhook or get the listing of existing webhooks 🔝

function RegisterWebhook {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the webhook name.")]
        [string]$webHookName,  # Webhook Name to register
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the webhook url.")]
        [string]$webHookURL,  # URL to register
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the Created By Name.")]
        [string]$createdBy,  # Registration done by
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the Security Key for your Webhook.")]
        [string]$functionKey  # Security key for the webhook
    )

    Write-Host "Inside RegisterWebhook"  # Log entry into the function

        # Ensure parameters are not empty
        Write-Host "Read Environment Variables" -ForegroundColor Cyan  # Log the file being used
        # Read the contents of the .env file
        $sitecoreEdgeApi = ${env:SITECORE_EDGE_API}

        Write-Host "Local variable bearerToken set with value '$bearerToken'."

        Write-Host "Calling Sitecore Edge API to regsiter the webhook"

        $headers = @{ Authorization = "Bearer $bearerToken" }

            # Prepare the body
            $body = @{
                label = $webHookName
                uri = $webHookURL
                method = "POST"
                headers = @{
                    "x-functions-key" = $functionKey
                }
                createdBy = $createdBy
                executionMode = "OnUpdate"
            } | ConvertTo-Json -Depth 10  # Convert the body to JSON            

            Write-Host "Request Body: $body"
            Write-Host "Headers: $headers"        

        try {
            $response = Invoke-WebRequest -Uri $sitecoreEdgeApi -Headers $headers -Method POST -Body $body -ContentType "application/json" -ErrorAction Stop
            $statusCode = $response.StatusCode

            # Print the raw response content
            # Write-Host "Response Content GetWebhookListing:"
            # Write-Host $response.Content                
        } catch {
            Write-Host "Error occurred in RegisterWebhook: $($_.Exception.Message)"
            if ($_.Exception.Response -ne $null) {
                $statusCode = $_.Exception.Response.StatusCode
            } else {
                throw "Unexpected error occurred: $($_.Exception.Message)"
            }
        }

        # Handle the status code and execute the next step
        switch ($statusCode) {
            201 {
                Write-Host "Webhook registered successfully. Response:"
                $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 10
                # Add your next step for success here
            }
            401 {
                Write-Host "Unauthorized access 401. Please check your bearer token."
                Write-Host "Response content: $($response.Content)"
                Write-Warning "JWT verification failed. Requesting new token..."
                # Add your next step for unauthorized access here
                $bearerToken=GetAccessToken
                Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                RegisterWebhook -webHookName $webHookName -webHookURL $webHookURL -createdBy $createdBy -functionKey $functionKey
            }
            default {
                if ($statusCode -match "Unauthorized") {
                    Write-Host "Unauthorized error detected. Please verify your credentials or token."
                    Write-Warning "JWT verification failed. Requesting new token..."
                    # Add your next step for handling unauthorized errors here
                    $bearerToken=GetAccessToken
                    Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                    RegisterWebhook -webHookName $webHookName -webHookURL $webHookURL -createdBy $createdBy -functionKey $functionKey
                } else {
                    Write-Host "Failed to retrieve webhooks. Status code: $statusCode"
                    Write-Host "Response content: $($response.Content)"
                    # Add your next step for other errors here
                }
            }
        }

        return $null  # Return null if not found     

}

Step 4: Integrate Script into Azure Pipeline

Configure your Azure DevOps pipeline to execute the PowerShell script as a task. Trigger this task manually or automatically during deployment or environment changes. The pipeline will:

  • Inject environment variables securely

  • Run the script to ensure the webhook is registered with a valid token

  • Enable quick switching between environments by changing only CLIENT_ID and CLIENT_SECRET

Here’s an example YAML configuration: 🔝

# Starter pipeline

trigger:
- feature/REGISTER-WEBHOOK

pool:
  vmImage: ubuntu-latest

variables:
  system.debug: true

- script: $PSVersionTable
  displayName: 'Check PowerShell Version'

- task: PowerShell@2
  displayName: 'Run PowerShell Script with Windows PowerShell 5.1'
  inputs:
    filePath: './Register-XMCloud-Webhook.ps1'
    failOnStderr: true
    pwsh: false  # Use Windows PowerShell instead of PowerShell Core

This ensures your webhooks are always registered and ready—with no manual steps required.

🏁Final Step: Test and Validate

After running the pipeline, verify that the webhook is registered by checking the Experience Edge API response or testing the webhook with a sample OnUpdate event from XM Cloud.

I have created a script GetWebhookListing that easily checks the list of webhooks registered with Experience Edge, removing the need for manual work. This automated solution simplifies the process, allowing you to quickly verify webhook registrations. By using this script, you improve efficiency and maintain smooth integration with Experience Edge 🔝

function GetWebhookListing {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Specifies the path of the .env file.")]
        [string]$filePath = ".env"  # Default path for the .env file
    )

    Write-Host "Inside GetWebhookListing"  # Log entry into the function

    $envFilePath = Resolve-Path "$PSScriptRoot\$filePath"  # Resolve the full path of the .env file

    if (Test-Path $envFilePath) {  # Check if the .env file exists
        if ($varName -ne "" -and $envFilePath -ne "") {  # Ensure parameters are not empty
            Write-Host "Using .env file: $envFilePath" -ForegroundColor Cyan  # Log the file being used
            # Read the contents of the .env file
            $sitecoreEdgeApi = GetEnvVariable -filePath $EnvFileName -varName "SITECORE_EDGE_API"

            # Write-Host "Local variable bearerToken set with value '$bearerToken'."

            Write-Host "Calling Sitecore Edge API to get webhooks..."

            $headers = @{ Authorization = "Bearer $bearerToken" }

            try {
                $response = Invoke-WebRequest -Uri $sitecoreEdgeApi -Headers $headers -Method GET -ErrorAction Stop
                $statusCode = $response.StatusCode

                # Print the raw response content
                # Write-Host "Response Content GetWebhookListing:"
                # Write-Host $response.Content                
            } catch {
                Write-Host "Error occurred: $($_.Exception.Message)"
                if ($_.Exception.Response -ne $null) {
                    $statusCode = $_.Exception.Response.StatusCode
                } else {
                    throw "Unexpected error occurred: $($_.Exception.Message)"
                }
            }

            # Handle the status code and execute the next step
            switch ($statusCode) {
                200 {
                    Write-Host "Successfully retrieved webhooks:"
                    $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 10
                    # Add your next step for success here
                }
                401 {
                    Write-Host "Unauthorized access 401. Please check your bearer token."
                    Write-Host "Response content: $($response.Content)"
                    Write-Warning "JWT verification failed. Requesting new token..."
                    # Add your next step for unauthorized access here
                    $bearerToken=GetAccessToken -filePath $EnvFileName
                    Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                    GetWebhookListing -filePath $EnvFileName
                }
                default {
                    if ($statusCode -match "Unauthorized") {
                        Write-Host "Unauthorized error detected. Please verify your credentials or token."
                        Write-Warning "JWT verification failed. Requesting new token..."
                        # Add your next step for handling unauthorized errors here
                        $bearerToken=GetAccessToken -filePath $EnvFileName
                        Write-Host "Local variable bearerToken received from GetAccessToken function is '$bearerToken'."  # Log success message
                        GetWebhookListing -filePath $EnvFileName
                    } else {
                        Write-Host "Failed to retrieve webhooks. Status code: $statusCode"
                        Write-Host "Response content: $($response.Content)"
                        # Add your next step for other errors here
                    }
                }
            }

            return $null  # Return null if not found
        } 
    } else {
        Write-Error "The .env file does not exist at the specified path: $envFilePath"  # Log error if file doesn't exist
        return $null  # Return null if file doesn't exist
    }
}

You can use this function with the following syntax: 🔝

GetWebhookListing -filePath $EnvFileName

In your previous blog on real-time sync, you discussed: 🔝

  • Listening to OnUpdate events

  • Pulling updated content from Experience Edge using GraphQL

  • Sending it to Sitecore Search via Ingestion API

This automated webhook setup is the missing piece that makes such syncs robust and self-sustaining.

🔚Conclusion: Save Time, Reduce Errors, Stay Synced

Manually registering Sitecore Experience Edge webhooks is no longer necessary. With a CI/CD pipeline and PowerShell script, you can:

  • Automate token creation

  • Reuse your webhook setup

  • Easily switch between different environments

This automation makes deployment faster, ensures everything works smoothly, and lets your team focus on new ideas instead of setting up infrastructure. 🔝

🙌 Engage With Us and Get the Code!

Have thoughts or suggestions 🔠 on automating Sitecore XM Cloud webhooks? Share your feedback 💬 in the comments or contribute to our GitHub repo! Scan the QR code below to access the full PowerShell script and Azure Pipelines YAML code in the Sitecore-Automation repository.

And if you enjoy this content, consider subscribing 📰 for more updates and insights. Your engagement means the world to me and helps me continue providing valuable resources! 🌟

🧾Credit/References

🔗Pingback

Sitecore Blog: Real-Time Integrations with XM CloudSitecore Community: Automating XM Cloud DeploymentsSitecore & Azure Functions Best Practices 🔝
Querying Experience Edge via GraphQLAutomating Sitecore Deployments with PowerShellAutomate Sitecore XM Cloud Webhooks with Azure Pipelines: Discover how to streamline Sitecore XM Cloud webhook registration using Azure Pipelines and a PowerShell script. Learn to automate access token generation and integrate with the Experience Edge API for efficient content management.
Automate Sitecore XM Cloud Webhooks with Azure Pipelines: Discover how to streamline Sitecore XM Cloud webhook registration using Azure Pipelines and a PowerShell script. Learn to automate access token generation and integrate with the Experience Edge API for efficient content management.Simplify Webhook Setup for Sitecore XM Cloud with PowerShell : Save time by automating Sitecore XM Cloud webhook registration with a custom PowerShell script. This guide covers Azure Pipelines integration and Experience Edge API for seamless webhook management.Azure Webhook Integration for Sitecore XM Cloud Made Easy: Learn how to integrate Azure webhooks with Sitecore XM Cloud using Azure Pipelines. Automate token generation and webhook registration with this step-by-step PowerShell solution.
How to Register Webhooks in Sitecore XM Cloud Automatically: Automate webhook registration for Sitecore XM Cloud using PowerShell and Azure Pipelines. This tutorial explains how to manage access tokens and connect with the Experience Edge API effortlessly.Streamline Access Token Automation for Sitecore XM Cloud: Simplify Sitecore XM Cloud webhook setup by automating access token generation with PowerShell. Learn how to use Azure Pipelines to register webhooks with the Experience Edge API.PowerShell and Azure Pipelines for Sitecore Webhook Automation: Boost efficiency with a PowerShell script to automate Sitecore XM Cloud webhook registration. This guide shows how to use Azure Pipelines for seamless Experience Edge integration.
Guide to Sitecore Experience Edge Webhook Integration: Explore how to automate webhook registration for Sitecore XM Cloud using Azure Pipelines and PowerShell. Connect with the Experience Edge API for real-time content updates.Automating Client Credentials for Sitecore XM Cloud Webhooks: Learn to automate client credentials and webhook registration for Sitecore XM Cloud. This PowerShell and Azure Pipelines solution simplifies integration with the Experience Edge API.Real-Time Webhook Sync with Sitecore XM Cloud: Discover how to automate webhook registration for real-time content sync in Sitecore XM Cloud. Use PowerShell and Azure Pipelines to streamline Experience Edge API integration. 🔝
0
Subscribe to my newsletter

Read articles from Amit Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Amit Kumar
Amit Kumar

My name is Amit Kumar. I work as a hands-on Solution Architect. My experience allows me to provide valuable insights and guidance to organizations looking to leverage cutting edge technologies for their digital solutions.As a Solution Architect, I have extensive experience in designing and implementing robust and scalable solutions using server-side and client-side technologies. My expertise lies in architecting complex systems, integrating various modules, and optimizing performance to deliver exceptional user experiences. Additionally, I stay up-to-date with the latest industry trends and best practices to ensure that my solutions are always cutting-edge and aligned with business objectives.