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:

1{
2 "id": 1485,
3 "organisation_id": "your-organisation-id",
4 "site_id": "apple",
5 "primary_region_id": "us-east-1",
6 "ref": null,
7 "client": "web",
8 "status": "completed",
9 "html_url": "https://calibreapp.com/your-organisation-id/apple/snapshots/1485",
10 "url": "https://calibreapp.com/api/sites/apple/snapshots/1485.json",
11 "created_at": "2018-07-16T23:33:17.970Z",
12 "pages": [
13 {
14 "uuid": "xxx-123-456-789-xxx",
15 "name": "iPhone X",
16 "status": "completed",
17 "endpoint": "https://www.apple.com/iphone-x/",
18 "canonical": false,
19 "profile": "iPhone 6, 3G connection",
20 "profile_uuid": "xxx-123-456-789-xxx",
21 "metrics": [
22 {
23 "name": "speed_index",
24 "value": 14097
25 },
26 {
27 "name": "visually_complete",
28 "value": 21317
29 },
30 {
31 "name": "lighthouse-seo-score",
32 "value": 75
33 },
34 {
35 "name": "lighthouse-best-practices-score",
36 "value": 75
37 },
38 {
39 "name": "lighthouse-accessibility-score",
40 "value": 87
41 },
42 {
43 "name": "lighthouse-performance-score",
44 "value": 10
45 },
46 {
47 "name": "js-parse-compile",
48 "value": 6888
49 },
50 {
51 "name": "time-to-first-byte",
52 "value": 128
53 },
54 {
55 "name": "first-contentful-paint",
56 "value": 4433
57 },
58 {
59 "name": "first-meaningful-paint",
60 "value": 11649
61 },
62 {
63 "name": "firstRender",
64 "value": 3724
65 },
66 {
67 "name": "dom-size",
68 "value": 2110
69 },
70 {
71 "name": "estimated-input-latency",
72 "value": 859
73 },
74 {
75 "name": "consistently-interactive",
76 "value": 22607
77 },
78 {
79 "name": "first-interactive",
80 "value": 21248
81 },
82 {
83 "name": "json_body_size_in_bytes",
84 "value": 2901
85 },
86 {
87 "name": "json_size_in_bytes",
88 "value": 1747
89 },
90 {
91 "name": "image_body_size_in_bytes",
92 "value": 863765
93 },
94 {
95 "name": "image_size_in_bytes",
96 "value": 863790
97 },
98 {
99 "name": "font_body_size_in_bytes",
100 "value": 402256
101 },
102 {
103 "name": "font_size_in_bytes",
104 "value": 404248
105 },
106 {
107 "name": "js_body_size_in_bytes",
108 "value": 1815834
109 },
110 {
111 "name": "js_size_in_bytes",
112 "value": 425500
113 },
114 {
115 "name": "css_body_size_in_bytes",
116 "value": 1329225
117 },
118 {
119 "name": "css_size_in_bytes",
120 "value": 93630
121 },
122 {
123 "name": "html_body_size_in_bytes",
124 "value": 425563
125 },
126 {
127 "name": "html_size_in_bytes",
128 "value": 42739
129 },
130 {
131 "name": "page_wait_timing",
132 "value": 1740
133 },
134 {
135 "name": "page_size_in_bytes",
136 "value": 2530039
137 },
138 {
139 "name": "page_body_size_in_bytes",
140 "value": 5537450
141 },
142 {
143 "name": "asset_count",
144 "value": 59
145 },
146 {
147 "name": "onload",
148 "value": 22592
149 },
150 {
151 "name": "oncontentload",
152 "value": 12595
153 }
154 ],
155 "budget_alerts": null,
156 "artifacts": {
157 "filmstrip": {
158 "thumbs": [
159 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1682063.892.jpg",
160 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1682532.696.jpg",
161 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1685799.232.jpg",
162 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1686499.204.jpg",
163 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1686899.188.jpg",
164 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1687299.172.jpg",
165 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1688782.446.jpg",
166 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1690182.39.jpg",
167 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1691349.01.jpg",
168 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1692448.966.jpg",
169 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1693715.582.jpg",
170 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1703281.866.jpg",
171 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1703365.196.jpg",
172 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1703381.862.jpg"
173 ],
174 "video": "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/screencast.mp4"
175 },
176 "har": "https://calibre-screenshots-prod.s3.amazonaws.com/5e7ebd8f932d99e2a017fd1e16551221/https___www_apple_com_iphone_x_/har/20180716233507265.json?X-Amz-Expires=3600&X-Amz-Date=20180716T233521Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAITFRYATLYAH7W3VQ/20180716/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=be3e0818f2b8c24114d2813faf83b11e3a6d6b8084c84fb44ecde27104215045"
177 }
178 },
179 {
180 "uuid": "xxx-123-456-789-xxx",
181 "name": "iPhone 6",
182 "status": "completed",
183 "endpoint": "https://www.apple.com/iphone-6/",
184 "canonical": false,
185 "profile": "iPhone 6, 3G connection",
186 "profile_uuid": "xxx-123-456-789-xxx",
187 "metrics": [
188 {
189 "name": "speed_index",
190 "value": 8517
191 },
192 {
193 "name": "visually_complete",
194 "value": 11477
195 },
196 {
197 "name": "lighthouse-seo-score",
198 "value": 68
199 },
200 {
201 "name": "lighthouse-best-practices-score",
202 "value": 68
203 },
204 {
205 "name": "lighthouse-accessibility-score",
206 "value": 93
207 },
208 {
209 "name": "lighthouse-performance-score",
210 "value": 26
211 },
212 {
213 "name": "js-parse-compile",
214 "value": 1501
215 },
216 {
217 "name": "time-to-first-byte",
218 "value": 108
219 },
220 {
221 "name": "first-contentful-paint",
222 "value": 3470
223 },
224 {
225 "name": "first-meaningful-paint",
226 "value": 8449
227 },
228 {
229 "name": "firstRender",
230 "value": 2398
231 },
232 {
233 "name": "dom-size",
234 "value": 727
235 },
236 {
237 "name": "estimated-input-latency",
238 "value": 17
239 },
240 {
241 "name": "consistently-interactive",
242 "value": 24364
243 },
244 {
245 "name": "first-interactive",
246 "value": 11378
247 },
248 {
249 "name": "json_body_size_in_bytes",
250 "value": 1716
251 },
252 {
253 "name": "json_size_in_bytes",
254 "value": 1011
255 },
256 {
257 "name": "image_body_size_in_bytes",
258 "value": 1968564
259 },
260 {
261 "name": "image_size_in_bytes",
262 "value": 1973146
263 },
264 {
265 "name": "font_body_size_in_bytes",
266 "value": 346672
267 },
268 {
269 "name": "font_size_in_bytes",
270 "value": 348381
271 },
272 {
273 "name": "js_body_size_in_bytes",
274 "value": 1077849
275 },
276 {
277 "name": "js_size_in_bytes",
278 "value": 271680
279 },
280 {
281 "name": "css_body_size_in_bytes",
282 "value": 720939
283 },
284 {
285 "name": "css_size_in_bytes",
286 "value": 62227
287 },
288 {
289 "name": "html_body_size_in_bytes",
290 "value": 51148
291 },
292 {
293 "name": "html_size_in_bytes",
294 "value": 9958
295 },
296 {
297 "name": "page_wait_timing",
298 "value": 198
299 },
300 {
301 "name": "page_size_in_bytes",
302 "value": 2666403
303 },
304 {
305 "name": "page_body_size_in_bytes",
306 "value": 4166888
307 },
308 {
309 "name": "asset_count",
310 "value": 73
311 },
312 {
313 "name": "onload",
314 "value": 28932
315 },
316 {
317 "name": "oncontentload",
318 "value": 7034
319 }
320 ],
321 "budget_alerts": null,
322 "artifacts": {
323 "filmstrip": {
324 "thumbs": [
325 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1683338.5.jpg",
326 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1683999.304.jpg",
327 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1685749.234.jpg",
328 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1686815.858.jpg",
329 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1686882.522.jpg",
330 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1688649.118.jpg",
331 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1689749.074.jpg",
332 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1691798.992.jpg",
333 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1693432.26.jpg",
334 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1693732.248.jpg",
335 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694015.57.jpg",
336 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694032.236.jpg",
337 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694098.9.jpg",
338 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694315.558.jpg",
339 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694498.884.jpg",
340 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694815.538.jpg"
341 ],
342 "video": "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/screencast.mp4",
343 "gif": "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/screencast.gif"
344 },
345 "har": "https://calibre-screenshots-prod.s3.amazonaws.com/f477b5ec59c5a357f1f7d756b23c4645/https___www_apple_com_iphone_6_/har/20180716233446500.json?X-Amz-Expires=3600&X-Amz-Date=20180716T233521Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAITFRYATLYAH7W3VQ/20180716/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=86e1fe079e3ce90c81bf81a62369c71c03c31640412cd22dce92c1996f4247b7"
346 }
347 }
348 ]
349}
1{
2 "id": "abc123",
3 "url": "http://calibreapp.com/teams/your-team/your-site/budgets/abc123",
4 "organisation_id": "your-organisation",
5 "site_id": "your-site",
6 "name": "Largest Contentful Paint",
7 "abbreviated_name": "LCP",
8 "value": 3000,
9 "formatted": "3s",
10 "status": "At risk",
11 "threshold": "GreaterThan",
12 "generated_at": "2020-09-30T05:33:17.970Z",
13 "budgets": [
14 {
15 "profile": {
16 "id": "pr123",
17 "name": "Chrome Desktop"
18 },
19 "page": {
20 "id": "p123",
21 "name": "Marketing Home",
22 "previews": []
23 },
24 "status": "At risk",
25 "value": 2850,
26 "formatted": "2.85s"
27 },
28 {
29 "profile": {
30 "id": "pr456",
31 "name": "MotoG4, 3G connection"
32 },
33 "page": {
34 "id": "p123",
35 "name": "Marketing Home",
36 "previews": []
37 },
38 "status": "Met",
39 "value": 2400,
40 "formatted": "2.4s"
41 },
42 {
43 "profile": {
44 "id": "pr789",
45 "name": "iPhone, 4G LTE"
46 },
47 "page": {
48 "id": "p123",
49 "name": "Marketing Home",
50 "previews": []
51 },
52 "status": "Met",
53 "value": 2400,
54 "formatted": "2.4s"
55 }
56 ]
57}

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:

1// Example built for express.js, using buffer-equal-constant-time
2// Requires environment variable containing the secret: process.env.SECRET_TOKEN
3
4const crypto = require('crypto')
5const bufferEq = require('buffer-equal-constant-time')
6
7const verifyCalibreHMAC = (req, res, next) => {
8 const payload = req.body()
9 const payloadSignature = req.header('Calibre-HMAC-SHA256-Signature')
10
11 let hmac = crypto.createHmac('sha256', process.env.SECRET_TOKEN)
12 hmac.write(payload)
13 hmac.end()
14
15 const signature = hmac.read()
16
17 if (bufferEq(new Buffer(signature), new Buffer(payloadSignature))) {
18 next()
19 } else {
20 throw new Error('HMAC Signatures do not match')
21 }
22}
23
24// Express will downcase headers
25app.use(verifyCalibreHMAC)
26app.post('/calibre-webhook', (req, res) => {
27 res.send('We have a valid hmac signature')
28})
1// SECRET_TOKEN is an environment variable
2
3post '/calibre-webhook' do
4 request.body.rewind
5 payload_body = request.body.read
6 verify_signature(payload_body)
7
8 # Safely use the JSON
9end
10
11def verify_signature(payload_body)
12 signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
13 return halt 500, "Signatures didn't match!" unless ActiveSupport::SecurityUtils.secure_compare(signature, request.env['Calibre-HMAC-SHA256-Signature'])
14end

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.