Using webhooks for automatic updates

In this tutorial, you'll see how webhooks allow you to integrate Kentico Cloud with other software applications and automate your processes. Think of webhooks as programmatic notifications that let your application know when something changes inside your Kentico Cloud project.

Table of contents

    As an example of webhooks in action, when a new content item is published your application can automatically react in numerous ways, such as:

    • Invalidating the cache of your app to make sure users see the latest content.
    • Updating a search index of your project's content.
    • Triggering a new build process and redeploying your application.
    • Notifying your team by sending an email, posting a message to a Slack channel or moving a card inside Trello.
    • Scheduling a social media post featuring the newly published content item.

    How webhooks work in a nutshell

    • When something changes inside your project, we send an HTTP POST request to a URL you have specified.
    • The payload of the request contains structured information about the type of change and the affected content items.
    • The header of the request contains a secret token you can use to authenticate the message.
    • Your application's endpoint at the specified URL must process the request - parse the information in the payload and react accordingly.
    • If your application is down or fails to process the request correctly, we send it again according to the retry policy.

    Let's go through the process in more detail.

    Creating a webhook

    To register a new webhook in your project:

    1. In Kentico Cloud, choose Project settings from the app menu.
    2. Under Development, choose Webhooks.
    3. Click Create new Webhook.
    4. Type a name for the webhook. For example, Purge cache.
    5. Enter a publicly available URL address of your webhook endpoint, such as https://yourapp.com/webhook.
    6. Click Save.

    Registering a webhook in Kentico Cloud.

    Now, whenever your published content changes, we will send a notification to your webhook endpoint.

    When webhooks are called

    Webhooks are called as a result of actions done by users in a Kentico Cloud project. Whenever an action affects already published content, you get a notification.

    Here are a few examples of the actions that can trigger a webhook:

    • A user changes the description of an asset.
    • A content contributor rewrites some text in a content item.
    • A content contributor renames a few terms in a Taxonomy group.

    For the full list of actions, see the list of Types and operations in our API reference.

    Note: Updates to content made using the Content Management API can't trigger webhooks. The CM API can only work with content that is not published.

    Receiving notifications

    Once the webhook is registered, we will start sending HTTP POST notifications to the provided webhook URL. Receiving your notifications might take a few minutes or possibly even longer.

    Note that the notifications may sometimes come in batches because the content changes are processed dynamically based on load.

    Webhook call model

    The notifications come in the form of a JSON object with two attributes: message and data. message tells you why the notification came and data tells you which content items and taxonomy groups were affected.

    You can find the full webhook notification model description in the API reference.

    • Java
    import com.fasterxml.jackson.annotation.JsonProperty; public class KenticoCloudWebhookModel { @JsonProperty("message") Message message; @JsonProperty("data") Data data; public Message getMessage() { return message; } public Data getData() { return data; } } public class Message { @JsonProperty("id") String id; @JsonProperty("type") String type; @JsonProperty("operation") String operation; @JsonProperty("api_name") String apiName; @JsonProperty("project_id") String projectId; public String getId { return id; } public String getType { return type; } public String getOperation { return operation; } public String getApiName { return apiName; } public String getProjectId { return projectId; } } public class Data { @JsonProperty("items") List<Item> items; @JsonProperty("taxonomies") List<Taxonomy> taxonomies; public List<Item> getItems { return items; } public List<Taxonomy> getTaxonomies { return taxonomies; } } public class Item { @JsonProperty("id") String id; @JsonProperty("codename") String codename; @JsonProperty("language") String language; @JsonProperty("type") String type; public String getId { return id; } public String getCodename { return codename; } public String getLanguage { return language; } public String getType { return type; } } public class Taxonomy { @JsonProperty("id") String id; @JsonProperty("codename") String codename; public String getId { return id; } public String getCodename { return codename; } }
    import com.fasterxml.jackson.annotation.JsonProperty; public class KenticoCloudWebhookModel { @JsonProperty("message") Message message; @JsonProperty("data") Data data; public Message getMessage() { return message; } public Data getData() { return data; } } public class Message { @JsonProperty("id") String id; @JsonProperty("type") String type; @JsonProperty("operation") String operation; @JsonProperty("api_name") String apiName; @JsonProperty("project_id") String projectId; public String getId { return id; } public String getType { return type; } public String getOperation { return operation; } public String getApiName { return apiName; } public String getProjectId { return projectId; } } public class Data { @JsonProperty("items") List<Item> items; @JsonProperty("taxonomies") List<Taxonomy> taxonomies; public List<Item> getItems { return items; } public List<Taxonomy> getTaxonomies { return taxonomies; } } public class Item { @JsonProperty("id") String id; @JsonProperty("codename") String codename; @JsonProperty("language") String language; @JsonProperty("type") String type; public String getId { return id; } public String getCodename { return codename; } public String getLanguage { return language; } public String getType { return type; } } public class Taxonomy { @JsonProperty("id") String id; @JsonProperty("codename") String codename; public String getId { return id; } public String getCodename { return codename; } }
    • Java
    import com.fasterxml.jackson.annotation.JsonProperty; public class KenticoCloudWebhookModel { @JsonProperty("message") Message message; @JsonProperty("data") Data data; public Message getMessage() { return message; } public Data getData() { return data; } } public class Message { @JsonProperty("id") String id; @JsonProperty("type") String type; @JsonProperty("operation") String operation; @JsonProperty("api_name") String apiName; @JsonProperty("project_id") String projectId; public String getId { return id; } public String getType { return type; } public String getOperation { return operation; } public String getApiName { return apiName; } public String getProjectId { return projectId; } } public class Data { @JsonProperty("items") List<Item> items; @JsonProperty("taxonomies") List<Taxonomy> taxonomies; public List<Item> getItems { return items; } public List<Taxonomy> getTaxonomies { return taxonomies; } } public class Item { @JsonProperty("id") String id; @JsonProperty("codename") String codename; @JsonProperty("language") String language; @JsonProperty("type") String type; public String getId { return id; } public String getCodename { return codename; } public String getLanguage { return language; } public String getType { return type; } } public class Taxonomy { @JsonProperty("id") String id; @JsonProperty("codename") String codename; public String getId { return id; } public String getCodename { return codename; } }
    import com.fasterxml.jackson.annotation.JsonProperty; public class KenticoCloudWebhookModel { @JsonProperty("message") Message message; @JsonProperty("data") Data data; public Message getMessage() { return message; } public Data getData() { return data; } } public class Message { @JsonProperty("id") String id; @JsonProperty("type") String type; @JsonProperty("operation") String operation; @JsonProperty("api_name") String apiName; @JsonProperty("project_id") String projectId; public String getId { return id; } public String getType { return type; } public String getOperation { return operation; } public String getApiName { return apiName; } public String getProjectId { return projectId; } } public class Data { @JsonProperty("items") List<Item> items; @JsonProperty("taxonomies") List<Taxonomy> taxonomies; public List<Item> getItems { return items; } public List<Taxonomy> getTaxonomies { return taxonomies; } } public class Item { @JsonProperty("id") String id; @JsonProperty("codename") String codename; @JsonProperty("language") String language; @JsonProperty("type") String type; public String getId { return id; } public String getCodename { return codename; } public String getLanguage { return language; } public String getType { return type; } } public class Taxonomy { @JsonProperty("id") String id; @JsonProperty("codename") String codename; public String getId { return id; } public String getCodename { return codename; } }
    • C#
    using System; using Newtonsoft.Json; public class KenticoCloudWebhookModel { [JsonProperty("message")] public Message Message { get; set; } [JsonProperty("data")] public Data Data { get; set; } } public class Message { [JsonProperty("id")] public Guid Id { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("operation")] public string Operation { get; set; } [JsonProperty("api_name")] public string ApiName { get; set; } [JsonProperty("project_id")] public Guid ProjectId { get; set; } } public class Data { [JsonProperty("items")] public Item[] Items { get; set; } [JsonProperty("taxonomies")] public Taxonomy[] Taxonomies { get; set; } } public class Item { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("codename")] public string Codename { get; set; } [JsonProperty("language")] public string Language { get; set; } [JsonProperty("type")] public string Type { get; set; } } public class Taxonomy { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("codename")] public string Codename { get; set; } }
    using System; using Newtonsoft.Json; public class KenticoCloudWebhookModel { [JsonProperty("message")] public Message Message { get; set; } [JsonProperty("data")] public Data Data { get; set; } } public class Message { [JsonProperty("id")] public Guid Id { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("operation")] public string Operation { get; set; } [JsonProperty("api_name")] public string ApiName { get; set; } [JsonProperty("project_id")] public Guid ProjectId { get; set; } } public class Data { [JsonProperty("items")] public Item[] Items { get; set; } [JsonProperty("taxonomies")] public Taxonomy[] Taxonomies { get; set; } } public class Item { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("codename")] public string Codename { get; set; } [JsonProperty("language")] public string Language { get; set; } [JsonProperty("type")] public string Type { get; set; } } public class Taxonomy { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("codename")] public string Codename { get; set; } }

    Verifying notifications

    To verify the authenticity of the notifications, you need to generate a hash using the body of the notification and the secret key (you'll find the key in the configuration details of your webhook).

    The calculated hash should match the notification signature in the X-KC-Signature header that is sent with each notification. For example, a signature can look like this fRbrQ1lpBSRB9T3MckJ51HDdjQ8UuV3WnjqKqirSpW8=. The signature is a base64 encoded string generated using a hash-based message authentication code (HMAC) with SHA-256.

    For more examples on generating verification hashes, see the code samples in our API reference.

    Once you have received and verified the message, you can react to it and use the provided information. You might consider using webhooks for clearing the cache of your app, triggering a build process, or scheduling social media posts. See our blog posts for some inspiration.

    Worked examples 

    Learn how to integrate webhooks into your app's workflow from worked examples in our blog posts.

    Getting the latest content

    After you get a notification about changed content, you might want to explicitly request the new content from the Delivery API. You can do this by sending a standard request to the Delivery API with the X-KC-Wait-For-Loading-New-Content header.

    • Java
    import com.kenticocloud.delivery_core.*; import com.kenticocloud.delivery_rx.*; import io.reactivex.Observer; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Function; // Prepares an array to hold strongly-typed models List<TypeResolver<?>> typeResolvers = new ArrayList<>(); // Registers the type resolver for articles typeResolvers.add(new TypeResolver<>(Article.TYPE, new Function<Void, Article>() { @Override public Article apply(Void input) { return new Article(); } })); // Prepares the DeliveryService configuration object String projectId = "975bf280-fd91-488c-994c-2f04416e5ee3"; IDeliveryConfig config = DeliveryConfig.newConfig(projectId) .withDefaultQueryConfig(new QueryConfig(true, false)) .withTypeResolvers(typeResolvers); // Initializes a DeliveryService for Java projects IDeliveryService deliveryService = new DeliveryService(config); // Gets specific elements of an article using a simple request Article article = deliveryService.<Article>item("on_roasts") .get() .getItem(); // Gets specific elements of an article using RxJava2 deliveryService.<Article>item("on_roasts") .getObservable() .subscribe(new Observer<DeliveryItemResponse<Article>>() { @Override public void onSubscribe(Disposable d) { } @Override public void onNext(DeliveryItemResponse<Article> response) { // Gets the article Article article = response.getItem(); } @Override public void onError(Throwable e) { } @Override public void onComplete() { } });
    import com.kenticocloud.delivery_core.*; import com.kenticocloud.delivery_rx.*; import io.reactivex.Observer; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Function; // Prepares an array to hold strongly-typed models List<TypeResolver<?>> typeResolvers = new ArrayList<>(); // Registers the type resolver for articles typeResolvers.add(new TypeResolver<>(Article.TYPE, new Function<Void, Article>() { @Override public Article apply(Void input) { return new Article(); } })); // Prepares the DeliveryService configuration object String projectId = "975bf280-fd91-488c-994c-2f04416e5ee3"; IDeliveryConfig config = DeliveryConfig.newConfig(projectId) .withDefaultQueryConfig(new QueryConfig(true, false)) .withTypeResolvers(typeResolvers); // Initializes a DeliveryService for Java projects IDeliveryService deliveryService = new DeliveryService(config); // Gets specific elements of an article using a simple request Article article = deliveryService.<Article>item("on_roasts") .get() .getItem(); // Gets specific elements of an article using RxJava2 deliveryService.<Article>item("on_roasts") .getObservable() .subscribe(new Observer<DeliveryItemResponse<Article>>() { @Override public void onSubscribe(Disposable d) { } @Override public void onNext(DeliveryItemResponse<Article> response) { // Gets the article Article article = response.getItem(); } @Override public void onError(Throwable e) { } @Override public void onComplete() { } });
    • Java
    import com.kenticocloud.delivery; DeliveryOptions deliveryOptions = new DeliveryOptions(); deliveryOptions.setProjectId("975bf280-fd91-488c-994c-2f04416e5ee3"); deliveryOptions.setWaitForLoadingNewContent(true); DeliveryClient client = new DeliveryClient(deliveryOptions); ContentItemResponse item = client.getItem("on_roasts");
    import com.kenticocloud.delivery; DeliveryOptions deliveryOptions = new DeliveryOptions(); deliveryOptions.setProjectId("975bf280-fd91-488c-994c-2f04416e5ee3"); deliveryOptions.setWaitForLoadingNewContent(true); DeliveryClient client = new DeliveryClient(deliveryOptions); ContentItemResponse item = client.getItem("on_roasts");
    • JavaScript
    const KenticoCloud = require("kentico-cloud-delivery"); // Create strongly typed models according to https://developer.kenticocloud.com/docs/strongly-typed-models class Article extends KenticoCloud.ContentItem { constructor() { super(); } } const deliveryClient = new KenticoCloud.DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new KenticoCloud.TypeResolver("article", () => new Article()) ] }); deliveryClient.item("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .getObservable() .subscribe(response => console.log(response));
    const KenticoCloud = require("kentico-cloud-delivery"); // Create strongly typed models according to https://developer.kenticocloud.com/docs/strongly-typed-models class Article extends KenticoCloud.ContentItem { constructor() { super(); } } const deliveryClient = new KenticoCloud.DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new KenticoCloud.TypeResolver("article", () => new Article()) ] }); deliveryClient.item("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .getObservable() .subscribe(response => console.log(response));
    • C#
    using KenticoCloud.Delivery; // Initializes a client that retrieves the latest version of published content IDeliveryClient client = DeliveryClientBuilder .WithOptions(builder => builder .WithProjectId("975bf280-fd91-488c-994c-2f04416e5ee3") .UseProductionApi .WaitForLoadingNewContent .Build()) .Build(); // Gets a content item // Create strongly typed models according to https://developer.kenticocloud.com/docs/strongly-typed-models DeliveryItemResponse<object> response = await client.GetItemAsync<object>("on_roasts");
    using KenticoCloud.Delivery; // Initializes a client that retrieves the latest version of published content IDeliveryClient client = DeliveryClientBuilder .WithOptions(builder => builder .WithProjectId("975bf280-fd91-488c-994c-2f04416e5ee3") .UseProductionApi .WaitForLoadingNewContent .Build()) .Build(); // Gets a content item // Create strongly typed models according to https://developer.kenticocloud.com/docs/strongly-typed-models DeliveryItemResponse<object> response = await client.GetItemAsync<object>("on_roasts");
    • PHP
    <?php // Defined by Composer to include required libraries require __DIR__ . "/vendor/autoload.php"; use KenticoCloud\Delivery\DeliveryClient; $client = new DeliveryClient("<YOUR_PROJECT_ID>", null, true); $item = client->getItem("on_roasts");
    <?php // Defined by Composer to include required libraries require __DIR__ . "/vendor/autoload.php"; use KenticoCloud\Delivery\DeliveryClient; $client = new DeliveryClient("<YOUR_PROJECT_ID>", null, true); $item = client->getItem("on_roasts");
    • cURL
    curl --request GET \ --url https://deliver.kenticocloud.com/975bf280-fd91-488c-994c-2f04416e5ee3/items/on_roasts \ --header "X-KC-Wait-For-Loading-New-Content: true" \ --header "content-type: application/json"
    curl --request GET \ --url https://deliver.kenticocloud.com/975bf280-fd91-488c-994c-2f04416e5ee3/items/on_roasts \ --header "X-KC-Wait-For-Loading-New-Content: true" \ --header "content-type: application/json"
    • Ruby
    require "delivery-sdk-ruby" delivery_client = KenticoCloud::Delivery::DeliveryClient.new project_id: "975bf280-fd91-488c-994c-2f04416e5ee3" delivery_client.item("on_roasts") .request_latest_content .execute do |response| item = response.item end
    require "delivery-sdk-ruby" delivery_client = KenticoCloud::Delivery::DeliveryClient.new project_id: "975bf280-fd91-488c-994c-2f04416e5ee3" delivery_client.item("on_roasts") .request_latest_content .execute do |response| item = response.item end
    • TypeScript
    import { ContentItem, DeliveryClient, Fields, TypeResolver } from "kentico-cloud-delivery"; // Create strongly typed models according to https://developer.kenticocloud.com/docs/strongly-typed-models export class Article extends ContentItem { public title: Fields.TextField; public summary: Fields.TextField; public post_date: Fields.DateTimeField; public teaser_image: Fields.AssetsField; public related_articles: Article[]; } const deliveryClient = new DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new TypeResolver("article", () => new Article) ] }); deliveryClient.item<Article>("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .getObservable() .subscribe(response => console.log(response));
    import { ContentItem, DeliveryClient, Fields, TypeResolver } from "kentico-cloud-delivery"; // Create strongly typed models according to https://developer.kenticocloud.com/docs/strongly-typed-models export class Article extends ContentItem { public title: Fields.TextField; public summary: Fields.TextField; public post_date: Fields.DateTimeField; public teaser_image: Fields.AssetsField; public related_articles: Article[]; } const deliveryClient = new DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new TypeResolver("article", () => new Article) ] }); deliveryClient.item<Article>("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .getObservable() .subscribe(response => console.log(response));

    Including the header will cause the Delivery API to explicitly fetch new content. Without the header, the Delivery API might return stale content (if cached by the CDN) for performance reasons while fetching latest content. To find out more, see Serving stale content in the Fastly documentation.

    Retry policy

    If your application responds with a 20X HTTP status code, the notification delivery is considered successful. Any other status code or a request timeout (which occurs after 60 seconds) will result in a retry policy.

    On the first unsuccessful delivery, we will try to send the notification again in 1 minute. If the delivery is unsuccessful, the delay between resending the notification increases exponentially to a maximum of 1 hour. The specific delay intervals are (in minutes): 1, 2, 4, 8, 16, 32, 60. When the delay reaches 60 minutes, we try to deliver the notification every hour for up to 3 days, after which the notification is removed from the queue.

    Email notifications 

    We will send email notifications to users with the Manage APIs capability in these cases:

    • Notification delivery repeatedly failing for 1 hour. This email is sent only once for each registered webhook.
    • Notification delivery repeatedly failing for 3 days. Note that we will not attempt to deliver the notification again.
    • Notification delivery was successful after failed attempts. This email is only sent if you previously received an email notification about a failed delivery.

    Note: All notifications are delivered in the order they were created. For example, if a notification is successfully delivered after 4 minutes, the notifications created after it will follow in the original order.

    Debugging webhooks

    If you get an email that a webhook is failing, you might want to know more about that webhook and what the problem is. For that, you can find more information inside Kentico Cloud in your list of webhooks under Project settings -> Webhooks. 

    For an overview of the health of your webhooks, each webhook in your list has a colored status next to its name:

    • Light grey – Ready for message. This appears for newly created webhooks before any change to published content has been made (so no notification has been sent).
    • Green – Working. This appears for webhooks that have properly delivered notifications.
    • Red – Failing. This appears for webhooks that have not been delivered properly (received a response other than a 20X HTTP status code). These webhook notifications are still being sent based on the retry policy.
    • Grey – Dead. This appears for webhooks where delivery has repeatedly failed and the retry policy has been exhausted so no more notifications will be sent.

    For more information about each webhook, click on Debugging. You'll find a list of all notifications with attempts at sending within the last 3 days sorted from newest to oldest. You can filter the list to show everything, only failures (at any time in sending the message), or only active failures (where the last response was a failure). Click Refresh to reload the list.

    Each notification in the list will show:

      • How many times the delivery has been attempted
      • A button () to see the most recent response
    • The date and time when the most recent delivery attempt was made
    • A button () to see the content of the sent notification with the chance to copy it

    What's next?