Webhooks


Calibre’s webhook support allows to receive notifications when a Snapshot is created and when a budget changes status for a given Site. This article explains how to create webhooks manually and with automations.

Adding a webhook manually#

  1. Create a new webhook by navigating to Settings → Integrations → Add a Webhook for a given Site and pasting in the payload URL (data will be delivered as application/json via HTTP POST).

  2. Choose the type of notifications:

  • Snapshot notifications: sent when a new Snapshot is completed.
  • Budget notifications: sent when a metric Budget is exceeded, met or at risk.
  1. Create the integration by clicking the Save button.
Webhook creation form

Adding a webhook programmatically#

Webhooks (and other integrations) can be managed programmatically using the Node.js API. Check the Integrations API reference for more information.

Example JSON output#

Webhook data is delivered as JSON (application/json) via HTTP POST to an endpoint of your choice. Here is an example of the output:

{
	"id": 1485,
	"organisation_id": "your-organisation-id",
	"site_id": "apple",
	"primary_region_id": "us-east-1",
	"ref": null,
	"client": "web",
	"status": "completed",
	"html_url": "https://calibreapp.com/your-organisation-id/apple/snapshots/1485",
	"url": "https://calibreapp.com/api/sites/apple/snapshots/1485.json",
	"created_at": "2018-07-16T23:33:17.970Z",
	"pages": [
		{
			"uuid": "xxx-123-456-789-xxx",
			"name": "iPhone X",
			"status": "completed",
			"endpoint": "https://www.apple.com/iphone-x/",
			"canonical": false,
			"profile": "iPhone 6, 3G connection",
			"profile_uuid": "xxx-123-456-789-xxx",
			"metrics": [
				{
					"name": "speed_index",
					"value": 14097
				},
				{
					"name": "visually_complete",
					"value": 21317
				},
				{
					"name": "lighthouse-seo-score",
					"value": 75
				},
				{
					"name": "lighthouse-best-practices-score",
					"value": 75
				},
				{
					"name": "lighthouse-accessibility-score",
					"value": 87
				},
				{
					"name": "lighthouse-performance-score",
					"value": 10
				},
				{
					"name": "js-parse-compile",
					"value": 6888
				},
				{
					"name": "time-to-first-byte",
					"value": 128
				},
				{
					"name": "first-contentful-paint",
					"value": 4433
				},
				{
					"name": "first-meaningful-paint",
					"value": 11649
				},
				{
					"name": "firstRender",
					"value": 3724
				},
				{
					"name": "dom-size",
					"value": 2110
				},
				{
					"name": "estimated-input-latency",
					"value": 859
				},
				{
					"name": "consistently-interactive",
					"value": 22607
				},
				{
					"name": "first-interactive",
					"value": 21248
				},
				{
					"name": "json_body_size_in_bytes",
					"value": 2901
				},
				{
					"name": "json_size_in_bytes",
					"value": 1747
				},
				{
					"name": "image_body_size_in_bytes",
					"value": 863765
				},
				{
					"name": "image_size_in_bytes",
					"value": 863790
				},
				{
					"name": "font_body_size_in_bytes",
					"value": 402256
				},
				{
					"name": "font_size_in_bytes",
					"value": 404248
				},
				{
					"name": "js_body_size_in_bytes",
					"value": 1815834
				},
				{
					"name": "js_size_in_bytes",
					"value": 425500
				},
				{
					"name": "css_body_size_in_bytes",
					"value": 1329225
				},
				{
					"name": "css_size_in_bytes",
					"value": 93630
				},
				{
					"name": "html_body_size_in_bytes",
					"value": 425563
				},
				{
					"name": "html_size_in_bytes",
					"value": 42739
				},
				{
					"name": "page_wait_timing",
					"value": 1740
				},
				{
					"name": "page_size_in_bytes",
					"value": 2530039
				},
				{
					"name": "page_body_size_in_bytes",
					"value": 5537450
				},
				{
					"name": "asset_count",
					"value": 59
				},
				{
					"name": "onload",
					"value": 22592
				},
				{
					"name": "oncontentload",
					"value": 12595
				}
			],
			"budget_alerts": null,
			"artifacts": {
				"filmstrip": {
					"thumbs": [
						"https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1682063.892.jpg"
					],
					"video": "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/screencast.mp4"
				},
				"har": "https://calibre-screenshots-prod.s3.amazonaws.com/..."
			}
		}
	]
}

Webhook security and verification#

Calibre Webhooks can be securely validated using the HMAC cryptographic hash signature (Calibre-HMAC-SHA256-Signature header), which should be used to confirm that the incoming request originates from Calibre.

Setting a shared secret for your Site webhook#

  1. Navigate to the selected webhook (Site → Synthetic → Settings → Integrations).
  2. Use the automatically generated secret, or create your own.
  3. Press Save.

Verifying the secret#

We recommend using a third party webhook test service, or enabling verbose logging in order to verify the webhooks are delivered in the format as expected. In order to verify the Calibre request, you will need to use the shared secret from Site → Synthetic → Settings → Integrations.

You can find code examples for secret verification below:

// Example built for express.js, using buffer-equal-constant-time
// Requires environment variable containing the secret: process.env.SECRET_TOKEN

const crypto = require('crypto')
const bufferEq = require('buffer-equal-constant-time')

const verifyCalibreHMAC = (req, res, next) => {
	const payload = req.body()
	const payloadSignature = req.header('Calibre-HMAC-SHA256-Signature')

	let hmac = crypto.createHmac('sha256', process.env.SECRET_TOKEN)
	hmac.write(payload)
	hmac.end()

	const signature = hmac.read()

	if (bufferEq(new Buffer(signature), new Buffer(payloadSignature))) {
		next()
	} else {
		throw new Error('HMAC Signatures do not match')
	}
}

// Express will downcase headers
app.use(verifyCalibreHMAC)
app.post('/calibre-webhook', (req, res) => {
	res.send('We have a valid hmac signature')
})

In the examples above we have set the original secret as an environment variable. Never commit the secret to source control. We also advise against comparing hashes using the equal comparison operator (==) as it cannot protect against timing attacks.

On this page