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.

For example, 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 it works 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

Registering a webhook

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

When are webhooks 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:

  • User changes a description of an asset.
  • Content contributor rewrites some text in a content item.
  • 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.

Note that the notifications may sometimes come in batches because the content changes are processed dynamically every 0 to 2 minutes based on load.

Webhook call model

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

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

{
  "message": {
    "id": "77e3661a-6e8f-4d8c-963f-ccb4dbe08333",
    "type": "taxonomy",
    "operation": "upsert",
    "api_name": "delivery_production",
    "project_id": "975bf280-fd91-488c-994c-2f04416e5ee3"
  },
  "data": {
    "items": [
      {
        "language": "en-US",
        "codename": "hario_v60",
        "type": "brewer"
      },
      {
        "language": "en-US",
        "codename": "aeropress",
        "type": "brewer"
      }
    ],
    "taxonomies": [
      {
        "codename": "manufacturer"
      }
    ]
  }
}
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("language")]
    public string Language { get; set; }

    [JsonProperty("type")]
    public string Type { get; set; }

    [JsonProperty("codename")]
    public string Codename { get; set; }
}

public class Taxonomy
{
    [JsonProperty("codename")]
    public string Codename { get; set; }
}
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("language")
    String language;

    @JsonProperty("type")
    String type;

    @JsonProperty("codename")
    String codename;

    public String getLanguage { return language; }

    public String getType { return type; }

    public String getCodename { return codename; }
}

public class Taxonomy {
    @JsonProperty("codename")
    String codename;

    public String getCodename { return codename; }
}

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.

Reacting to notifications

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 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.

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'
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
// Tip: Generate strongly typed models via https://github.com/Kentico/cloud-generators-net
DeliveryItemResponse<object> response = await client.GetItemAsync<object>("on_roasts");
const KenticoCloud = require('kentico-cloud-delivery');

// Create strongly typed models according to https://github.com/Enngage/kentico-cloud-js/blob/master/doc/delivery.md#creating-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));
import { ContentItem, DeliveryClient, Fields, TypeResolver } from 'kentico-cloud-delivery';

// Create strongly typed models according to https://github.com/Enngage/kentico-cloud-js/blob/master/doc/delivery.md#creating-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 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_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() {
        }
    });
<?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');

Including the header will cause the Delivery API to wait while fetching new content. Without this header, the Delivery API returns old content (if cached by the CDN) while fetching new content to minimize wait time. To find out more, see Serving stale content in the Fastly CDN 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.

What's next?