Skip to main content

Webhooks

A Webhook is a method of extending a project with custom callbacks to third-party services. You can think of a webhook as a pointer to another server, or a Function-as-a-Service (FaaS), similar to AWS Lambda, Google Cloud Functions or Azure Functions.

Webhooks are available at the "Starter" plan level and above.

Webhooks can be found in the Settings tab of your project dashboard. Select Webhooks on the left side of your settings page.

Configuring a Webhook

Secret - The private key that you use to verify your Webhooks on the receiving end

Webhooks - A list of Webhook configurations. You can create as many Webhooks as you need.

Webhook Url - The target URL to be called when the Webhook is triggered

Resources - Built-in constructs "Assets" and  "Sites" and your own project's unique content types for which actions can be configured. Resource selections can be general using * or highly specific and selecting individual sites or content types.

Sites

You can configure actions on all sites by using "Site: *"

Content

You can configure actions on all content types by using  "Content: *"

Actions - Events in a project that trigger the Webhook

Sites

Actions available for sites: "Create", "Update", "Delete", "Publish/start" "Publish/success", "Publish/failure".

Content Types

Actions available for content types: "Create", "Update", "Delete".

Additional HTTP Headers - Key / Value pairs to be sent along as supplementary headers with the Webhook. These might be used to provide additional context to the receiving service.

Reasons you might use a Webhook

  • Notify a slack channel every time you publish a specific static site
  • Use a serverless function to enrich your article content

Testing Webhooks

You can use https://webhook.site to test your Webhooks before you build your own service. Below is an example of the Webhook payload that is sent to the configured "Webhook Url".

{
"action": "content:create",
"meta": {
"projectId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"userId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
},
"data": {
"contentId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"contentTypeName": "article",
"isSingleton": false
}
}

Verifying Webhooks

The takeshape-signature header included in each signed event contains a timestamp and a signature.

takeshape-signature	t=1630005165,v1=38db6a613b5b59efd09edf7657ac7bf51746c1c5b288a38223e1b591ed8ff3f7

Signatures are generated using a hash-based message authentication code (HMAC) with SHA-256.

Step 1: Extract the timestamp and signatures from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a name and value pair.

The value for t corresponds to the timestamp, and v1 corresponds to the signature.

const values = new Map(signatureHeader.split(',').map(pair => pair.split('=')));
const timestamp = values.get('t');
const signature = values.get('v1');

Step 2: Prepare the signedPayload string The signedPayload string is created by concatenating:

const signedString = `${timestamp}.${bodyJson}`;

Step 3: Determine the expected signature Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the signedPayload string as the message.

import {createHmac} from 'crypto';
const expectedSig = createHmac('sha256', secret).update(signedString).digest('hex');

Step 4: Compare the signatures Compare the signature (or signatures) in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

if (timingSafeEqual(signature, expectedSignature)) {
// webhook is authenticated
}

Complete example:

import {createHmac, timingSafeEqual} from 'crypto';

const signatureHeader = req.headers['takeshape-signature'];
const bodyJson = req.rawBody; // unparsed body string

const values = new Map(signatureHeader.split(',').map(pair => pair.split('=')));
const timestamp = values.get('t');
const signature = values.get('v1');

const signedString = `${timestamp}.${bodyJson}`;
const expectedSignature = createHmac('sha256', secret).update(signedString).digest('hex');

if (timingSafeEqual(signature, expectedSignature)) {
// webhook is authenticated
}