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
}