Using webhooks for automatic updates

In this tutorial, you'll see how webhooks allow you to integrate Kentico Kontent 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 Kontent 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 the search index for 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.

    Using webhooks, you can also react to workflow changes in content items to accomplish tasks such as:

    • Automatically sending content for translation and receive notifications when the translation is done.
    • Notifying reviewers that content is ready for review.

    How webhooks work in a nutshell

    • When something changes inside your project, we send an HTTP POST request to the URL you specify.
    • 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 Kontent, choose Project settings from the app menu.
    2. Under Development, choose Webhooks.
    3. Click Create new Webhook.
    4. Enter a name for the webhook, such as Purge cache.
    5. Enter a publicly available URL address of your webhook endpoint, such as https://myapp.com/webhook.
    6. (Optional) Choose the events to trigger the webhook. By default, the events related to content items and taxonomy are selected.
    7. Click Save.

    Webhook with default trigger events

    Registering a webhook in Kentico Kontent.

    Now, whenever content in your project changes and your webhook is set to watch for that kind of change (you've set it as a trigger), 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 Kontent project. For each webhook in your project, you can choose a set of user actions (events) that will trigger the webhook.

    You can choose events from the following trigger categories:

    • Workflow steps – trigger the webhook when content items are moved to one of the specified workflow steps.
    • Content items
      • Publish event – triggers the webhook when a change in your project affects published content.
      • Unpublish event – triggers the webhook when a content item is unpublished.
    • Taxonomy groups
      • Create or Update – triggers the webhook when taxonomy groups are created or modified
      • Delete event – triggers the webhook when taxonomy groups are deleted.
      • Restore event – triggers the webhook when taxonomy groups are restored after being deleted.

    For instance, if your webhook is set to watch for the Publish event of content items, you'll get a notification whenever an action affects already published content. Here are a few examples of actions that can trigger such webhooks:

    • A contributor changes the description of an asset that's used in a published item.
    • A contributor renames or reorders a few terms in a taxonomy group that's used in a published item.
    • A user publishes a content item.

    For the full list of events that can trigger webhooks, see the list of types and operations in webhooks reference.

    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 properties: message and data. message tells you why the notification came and data tells you which content items and taxonomy groups were affected.

    • 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; } }

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

    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. To do this, send a standard request to the Delivery API with the X-KC-Wait-For-Loading-New-Content header set to true.

    If the requested content has changed since the last request, the header determines whether to wait while fetching content. By default, when the header is not set, the API serves stale content (if cached by the CDN) while it's fetching the new content to minimize wait time.

    • 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 KontentDelivery = require("@kentico/kontent-delivery"); // Create strongly typed models according to https://docs.kontent.ai/strongly-typed-models class Article extends KontentDelivery.ContentItem { constructor() { super(); } } const deliveryClient = new KontentDelivery.DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new KontentDelivery.TypeResolver("article", (rawData) => new Article()) ] }); deliveryClient.item("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .toObservable() .subscribe(response => console.log(response));
    const KontentDelivery = require("@kentico/kontent-delivery"); // Create strongly typed models according to https://docs.kontent.ai/strongly-typed-models class Article extends KontentDelivery.ContentItem { constructor() { super(); } } const deliveryClient = new KontentDelivery.DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new KontentDelivery.TypeResolver("article", (rawData) => new Article()) ] }); deliveryClient.item("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .toObservable() .subscribe(response => console.log(response));
    • C#
    using Kentico.Kontent.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://docs.kontent.ai/strongly-typed-models DeliveryItemResponse<object> response = await client.GetItemAsync<object>("on_roasts");
    using Kentico.Kontent.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://docs.kontent.ai/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 Kentico\Kontent\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 Kentico\Kontent\Delivery\DeliveryClient; $client = new DeliveryClient("<YOUR_PROJECT_ID>", null, true); $item = client->getItem("on_roasts");
    • cURL
    curl --request GET \ --url https://deliver.kontent.ai/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.kontent.ai/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 = Kentico::Kontent::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 = Kentico::Kontent::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, Elements, TypeResolver } from "@kentico/kontent-delivery"; // Create strongly typed models according to https://docs.kontent.ai/strongly-typed-models export class Article extends ContentItem { public title: Elements.TextElement; public summary: Elements.TextElement; public post_date: Elements.DateTimeElement; public teaser_image: Elements.AssetsElement; public related_articles: Article[]; } const deliveryClient = new DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new TypeResolver("article", (rawData) => new Article) ] }); deliveryClient.item<Article>("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .toObservable() .subscribe(response => console.log(response));
    import { ContentItem, DeliveryClient, Elements, TypeResolver } from "@kentico/kontent-delivery"; // Create strongly typed models according to https://docs.kontent.ai/strongly-typed-models export class Article extends ContentItem { public title: Elements.TextElement; public summary: Elements.TextElement; public post_date: Elements.DateTimeElement; public teaser_image: Elements.AssetsElement; public related_articles: Article[]; } const deliveryClient = new DeliveryClient({ projectId: "975bf280-fd91-488c-994c-2f04416e5ee3", typeResolvers: [ new TypeResolver("article", (rawData) => new Article) ] }); deliveryClient.item<Article>("on_roasts") .queryConfig({ waitForLoadingNewContent: true }) .toObservable() .subscribe(response => console.log(response));

    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 Kontent 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?