Webhooks

A Webhook notifies another system via a HTTP POST request about a change in Livingdocs, e.g. when a document has been published. You can configure multiple Webhooks that are called on only one or multiple events.

Configuration

The Webhooks feature is enabled by default, but can get disabled via Server Configuration.

One or multiple Webhooks are configured via Project Config (see example below):

// Project Config settings.webhooks
webhooks: {
  active: true
  configurations: [
    {
      // Unique handle per project
      handle: 'my-webhook',
      // UI label
      label: 'My Webhook',
      // UI description
      description: 'A description for future self and coworkers',
      // URL to call with a POST request when one of the specified events occurs
      url: 'https://example.com/my-webhook-endpoint',
      // Sign request with HTTP header 'x-livingdocs-signature'
      //   1) undefined                               -> No signature
      //   2) 'a-secret-token-to-sign-the-request'    -> Sign request with token
      //   3) {$secretRef: {name: 'webhook-local'}}   -> Sign request with project secret
      // See below: Validate Webhook Signature on your Endpoint
      secret: 'a-secret-token-to-sign-the-request',
      // Enable webhook
      active: true,
      // The webhook is triggered only for the specified events when both
      // the conditions and change filters are met
      events: [
        'document.create',
        {
          name: 'document.update',
          conditions: {
            contentTypes: [
              'regular'
            ]
          },
          changeFilter: {
            metadataProperties: [
              'title'
            ]
          }
        }
      ]
    }
  ]
}

Events

Webhooks are triggered only when one of the specified events occurs. For each webhook, multiple events can be configured.

events: [
  'document.create',
  {
    name: 'document.update'
  }
]

The following events are supported:

  • document.create (Added in: release-2024-01)
  • document.delete (Added in: release-2024-01)
  • document.publish
  • document.unpublish
  • document.update
  • document.build
  • document.build.draft
  • publication.update (Added in: release-2024-03)
  • mediaLibraryEntry.create
  • mediaLibraryEntry.update
  • mediaLibraryEntry.archive
  • mediaLibraryEntry.revoke
  • mediaLibraryEntry.active (Added in: release-2024-03)
  • mediaLibraryEntry.invalid (Added in: release-2024-03)

Conditions

For more precise control over the triggering of webhooks, additional conditions can be defined for events. Webhooks will only be triggered if all conditions for an event are met.

Three types of conditions are supported:

Content Types

Conditions on content types enable the filtering of events from documents with a specific content type. Multiple content types can be specified, of which one must match for the webhook to be triggered.

events: [
  {
    name: 'document.publish',
    conditions: {
      contentTypes: [
        'regular'
      ]
    }
  }
]

Content type conditions are applicable to all document and publication events, including:

  • document.create
  • document.delete
  • document.publish
  • document.unpublish
  • document.update
  • document.build
  • document.build.draft
  • publication.update

Delivery Handles

Conditions on delivery handles enable the filtering of build events from documents with a specific delivery handle. Multiple delivery handles can be specified, of which one must match for the webhook to be triggered.

events: [
  {
    name: 'document.build',
    conditions: {
      deliveryHandles: [
        'web'
      ]
    }
  }
]

Delivery handle conditions are applicable to document build events, including:

  • document.build
  • document.build.draft

Metadata Properties

Conditions on metadata properties enable the filtering of events from documents with specific metadata properties. Multiple metadata properties can be specified, of which all must match for the webhook to be triggered.

events: [
  {
    name: 'document.publish',
    conditions: {
      metadataProperties: [
        {
          name: 'description',
          value: 'Some description'
        }
      ]
    }
  }
]

Please note that metadata property conditions are supported only for metadata plugins that store primitive types (boolean, number, string). Consequently, conditions can be registered for the following metadata plugins:

Metadata property conditions are applicable to all document and publication events, including:

  • document.create
  • document.delete
  • document.publish
  • document.unpublish
  • document.update
  • document.build
  • document.build.draft
  • publication.update

Change Filters

Similar to conditions, change filters enable more precise control over when to trigger webhooks.

However, change filters are not evaluated on the current document version but rather on the set of changes made to the document. The webhook is triggered only if at least one of the specified metadata properties has changed. Consequently, display filters are only supported for document.update events.

events: [
  {
    name: 'document.update',
    changeFilter: {
      metadataProperties: [
        'title'
      ]
    }
  }
]

Testing Webhooks

For quickly testing Webhooks we use https://webhook.site. It gives you an URL you can use to send webhooks to and look at all the requests in a web interface.

Webhook Config via Editor UI

We strongly propose adding Webhook configs via Project Config, but we also provide a UI in the Editor.

In the project menu, select “Project Setup”, then select “Webhooks” in the “Configuration” section of the menu. You can activate/deactivate all your configured webhooks using the setting “deliver webhooks on events”.

Webhooks Configuration

Add a new Webhook by clicking “Add Webhook” or edit an existing one by clicking on it’s entry in the list. This shows a form to configure the webhook.

Webhooks Configuration

Payload

Here is an example payload sent to your url set in the Webhook configuration.

document.create

{
  "event": "document.create",
  "deliveryId": "KIOSZpPUt8X6X-FDlnLTX",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "test-1",
  "documentId": 123,
  "actor": {
    "type": "user"
  }
}

document.delete

{
  "event": "document.delete",
  "deliveryId": "Vulsdw3-y2JLqnE13NjPa",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "test-1",
  "documentId": 123,
  "actor": {
    "type": "importer",
    "name": "Importer"
  }
}

document.publish

{
  "event": "document.publish",
  "deliveryId": "qy8qoxQPVCES1VDneg4FE", // unique id on every call
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "documentId": 179,
  "publicationEvent": {
    "createdAt": "2021-02-16T18:04:50.203Z",
    "contentType": "regular",
    "documentType": "article",
    "eventType": "publish",
    "publicationId": 174,
    "projectId": 3,
    "channelId": 4,
    "documentId": 179
  },
  "actor": {
    "type": "api-client",
    "name": "Publibot 3000"
  }
}

document.unpublish

{
  "event": "document.unpublish",
  "deliveryId": "p3QA1OhXkQGJepVTkdd1b",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "documentId": 179,
  "publicationEvent": {
    "createdAt": "2021-02-16T18:04:51.285Z",
    "contentType": "regular",
    "documentType": "article",
    "eventType": "unpublish",
    "publicationId": 174,
    "projectId": 3,
    "channelId": 4,
    "documentId": 179
  },
  "actor": {
    "type": "user"
  }
}

document.update

{
  "event": "document.update",
  "deliveryId": "B_-8BAfgvuJRKk7m_RSls",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "documentId": 179,
  "metadataPropertyChanges": [
    "title"
  ],
  "actor": {
    "type": "user"
  }
}

document.build

{
  "event": "document.build",
  "deliveryId": "2Xwe-gvuB_JsK3_4bJerT",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "documentId": 179,
  "publicationId": 322,
  "deliveryHandle": "web",
  "reportId": "2SG2MAA9RwPn",
  "actor": {
    "type": "user"
  }
}

document.build.draft

{
  "event": "document.build.draft",
  "deliveryId": "2Xwe-gvuB_JsK3_4bJerT",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "documentId": 179,
  "deliveryHandle": "web",
  "reportId": "2SG2MAA9RwPn",
  "actor": {
    "type": "user"
  }
}

publication.update

{
  "event": "publication.update",
  "deliveryId": "2um3G07jkaMVp7TudVzAY",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "documentId": 187,
  // Actor available for visiblePublicationDate changes,
  // but not when triggered by conditional components
  "actor": {
    "type": "api-client",
    "name": "Publibot 3000"
  }
}

mediaLibraryEntry.create

{
  "event": "mediaLibraryEntry.create",
  "deliveryId": "f8K1HZ-_fTIf5kT698NqG",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "mediaId": "PNIi08x4UdEA",
  "actor": {
    "type": "user"
  }
}

mediaLibraryEntry.update

{
  "event": "mediaLibraryEntry.update",
  "deliveryId": "_p6aKe9Len6WnlvANDJSz",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "mediaId": "PNIi08x4UdEA",
  "actor": {
    "type": "user"
  }
}

mediaLibraryEntry.archive

{
  "event": "mediaLibraryEntry.archive",
  "deliveryId": "gmWn5OfOsZzEiY3FNRgA4",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "mediaId": "PNIi08x4UdEA",
  "actor": {
    "type": "user"
  }
}

mediaLibraryEntry.revoke

{
  "event": "mediaLibraryEntry.revoke",
  "deliveryId": "J1FrTJTNKKRoGmjBlQ3_e",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "mediaId": "PNIi08x4UdEA",
  "actor": {
    "type": "user"
  }
}

mediaLibraryEntry.active

{
  "event": "mediaLibraryEntry.active",
  "deliveryId": "Ty7XErALFZSi1b96G76KB",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "mediaId": "PNIi08x4UdEA",
  // Actor available for immediate li-invalid and li-datetime-validity changes,
  // but not when triggered by li-datetime-validity events
  "actor": {
    "type": "user"
  }
}

mediaLibraryEntry.invalid

{
  "event": "mediaLibraryEntry.invalid",
  "deliveryId": "ql-_3zjSdCrHesEcAERFs",
  "projectId": 3,
  "projectHandle": "service",
  "webhookHandle": "handle",
  "mediaId": "PNIi08x4UdEA",
  // Actor available for immediate li-invalid and li-datetime-validity changes,
  // but not when triggered by li-datetime-validity events
  "actor": {
    "type": "user"
  }
}

Validate Webhook Signature on your Endpoint

If you have defined a secret for your Webhook, Livingdocs creates a signature of the payload and sends it with the request in the HTTP header x-livingdocs-signature. The signature is created using HMAC-SHA256 and will be sent in x-livingdocs-signature in the form sha256=<hex digest> for example sha256=d8a47af83666a771d57117aa28ef8d3243a3de43.

Here is sample code in JavaScript to validate the signature on your endpoint:

// set payload to the body received with the request to your endpoint
const payload = JSON.stringify(request.body)
const signature = request.headers['x-livingdocs-signature']

// secret needs to be the same you configured with the webhook in livingdocs
const secret = 'a-secret-token-to-sign-the-request' // you should not hardcode this but read it from an environment variable

// compute the hmac on the received payload using the same secret
const crypto = require('crypto')
const hmac = crypto.createHmac('sha256', secret)
const payloadSignature = Buffer.from(`sha256=${hmac.update(payload).digest('hex')}`, 'utf8')
const checksum = Buffer.from(signature, 'utf8')

// use timingSafeEqual to compare the signature sent from livingdocs with the computed checksum
if (crypto.timingSafeEqual(payloadSignature, checksum)) {
  // payload is valid, move on
} else {
  // abort, this is not a valid request
}