Webhooks


Webhooks can be added on a per Site basis and allow you to receive notifications when a Snapshot is created, when a budget is exceeded, met or at risk.

Adding a webhook manually

To create a new webhook, navigate to Settings → Integrations → Add a Webhook for a given Site and paste in the payload URL (data will be delivered as application/json via HTTP POST).

You can choose from:

  • Snapshot notifications: sent when a new Snapshot is completed.
  • Budget notifications: sent when a metric Budget is exceeded, met or at risk.

Create the integration by clicking the Save button.

Webhook creation form

Adding a webhook pragmatically

Webhooks and other integrations can be managed pragmatically using the Node.js API. See the Integrations page for more information.

Example JSON output

A webhook will be JSON (application/json) delivered via HTTP POST to an endpoint of your choosing. 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": "lighthouse-pwa-score",
48 "value": 45
49 },
50 {
51 "name": "js-parse-compile",
52 "value": 6888
53 },
54 {
55 "name": "time-to-first-byte",
56 "value": 128
57 },
58 {
59 "name": "first-contentful-paint",
60 "value": 4433
61 },
62 {
63 "name": "first-meaningful-paint",
64 "value": 11649
65 },
66 {
67 "name": "firstRender",
68 "value": 3724
69 },
70 {
71 "name": "dom-size",
72 "value": 2110
73 },
74 {
75 "name": "estimated-input-latency",
76 "value": 859
77 },
78 {
79 "name": "consistently-interactive",
80 "value": 22607
81 },
82 {
83 "name": "first-interactive",
84 "value": 21248
85 },
86 {
87 "name": "json_body_size_in_bytes",
88 "value": 2901
89 },
90 {
91 "name": "json_size_in_bytes",
92 "value": 1747
93 },
94 {
95 "name": "image_body_size_in_bytes",
96 "value": 863765
97 },
98 {
99 "name": "image_size_in_bytes",
100 "value": 863790
101 },
102 {
103 "name": "font_body_size_in_bytes",
104 "value": 402256
105 },
106 {
107 "name": "font_size_in_bytes",
108 "value": 404248
109 },
110 {
111 "name": "js_body_size_in_bytes",
112 "value": 1815834
113 },
114 {
115 "name": "js_size_in_bytes",
116 "value": 425500
117 },
118 {
119 "name": "css_body_size_in_bytes",
120 "value": 1329225
121 },
122 {
123 "name": "css_size_in_bytes",
124 "value": 93630
125 },
126 {
127 "name": "html_body_size_in_bytes",
128 "value": 425563
129 },
130 {
131 "name": "html_size_in_bytes",
132 "value": 42739
133 },
134 {
135 "name": "page_wait_timing",
136 "value": 1740
137 },
138 {
139 "name": "page_size_in_bytes",
140 "value": 2530039
141 },
142 {
143 "name": "page_body_size_in_bytes",
144 "value": 5537450
145 },
146 {
147 "name": "asset_count",
148 "value": 59
149 },
150 {
151 "name": "onload",
152 "value": 22592
153 },
154 {
155 "name": "oncontentload",
156 "value": 12595
157 }
158 ],
159 "budget_alerts": null,
160 "artifacts": {
161 "filmstrip": {
162 "thumbs": [
163 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1682063.892.jpg",
164 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1682532.696.jpg",
165 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1685799.232.jpg",
166 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1686499.204.jpg",
167 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1686899.188.jpg",
168 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1687299.172.jpg",
169 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1688782.446.jpg",
170 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1690182.39.jpg",
171 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1691349.01.jpg",
172 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1692448.966.jpg",
173 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1693715.582.jpg",
174 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1703281.866.jpg",
175 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1703365.196.jpg",
176 "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/1703381.862.jpg"
177 ],
178 "video": "https://calibre-screenshots-prod.s3.amazonaws.com/05efb55f-0d2b-4c25-afd4-06dd7038fada/video-timeline/screencast.mp4"
179 },
180 "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"
181 }
182 },
183 {
184 "uuid": "xxx-123-456-789-xxx",
185 "name": "iPhone 6",
186 "status": "completed",
187 "endpoint": "https://www.apple.com/iphone-6/",
188 "canonical": false,
189 "profile": "iPhone 6, 3G connection",
190 "profile_uuid": "xxx-123-456-789-xxx",
191 "metrics": [
192 {
193 "name": "speed_index",
194 "value": 8517
195 },
196 {
197 "name": "visually_complete",
198 "value": 11477
199 },
200 {
201 "name": "lighthouse-seo-score",
202 "value": 68
203 },
204 {
205 "name": "lighthouse-best-practices-score",
206 "value": 68
207 },
208 {
209 "name": "lighthouse-accessibility-score",
210 "value": 93
211 },
212 {
213 "name": "lighthouse-performance-score",
214 "value": 26
215 },
216 {
217 "name": "lighthouse-pwa-score",
218 "value": 36
219 },
220 {
221 "name": "js-parse-compile",
222 "value": 1501
223 },
224 {
225 "name": "time-to-first-byte",
226 "value": 108
227 },
228 {
229 "name": "first-contentful-paint",
230 "value": 3470
231 },
232 {
233 "name": "first-meaningful-paint",
234 "value": 8449
235 },
236 {
237 "name": "firstRender",
238 "value": 2398
239 },
240 {
241 "name": "dom-size",
242 "value": 727
243 },
244 {
245 "name": "estimated-input-latency",
246 "value": 17
247 },
248 {
249 "name": "consistently-interactive",
250 "value": 24364
251 },
252 {
253 "name": "first-interactive",
254 "value": 11378
255 },
256 {
257 "name": "json_body_size_in_bytes",
258 "value": 1716
259 },
260 {
261 "name": "json_size_in_bytes",
262 "value": 1011
263 },
264 {
265 "name": "image_body_size_in_bytes",
266 "value": 1968564
267 },
268 {
269 "name": "image_size_in_bytes",
270 "value": 1973146
271 },
272 {
273 "name": "font_body_size_in_bytes",
274 "value": 346672
275 },
276 {
277 "name": "font_size_in_bytes",
278 "value": 348381
279 },
280 {
281 "name": "js_body_size_in_bytes",
282 "value": 1077849
283 },
284 {
285 "name": "js_size_in_bytes",
286 "value": 271680
287 },
288 {
289 "name": "css_body_size_in_bytes",
290 "value": 720939
291 },
292 {
293 "name": "css_size_in_bytes",
294 "value": 62227
295 },
296 {
297 "name": "html_body_size_in_bytes",
298 "value": 51148
299 },
300 {
301 "name": "html_size_in_bytes",
302 "value": 9958
303 },
304 {
305 "name": "page_wait_timing",
306 "value": 198
307 },
308 {
309 "name": "page_size_in_bytes",
310 "value": 2666403
311 },
312 {
313 "name": "page_body_size_in_bytes",
314 "value": 4166888
315 },
316 {
317 "name": "asset_count",
318 "value": 73
319 },
320 {
321 "name": "onload",
322 "value": 28932
323 },
324 {
325 "name": "oncontentload",
326 "value": 7034
327 }
328 ],
329 "budget_alerts": null,
330 "artifacts": {
331 "filmstrip": {
332 "thumbs": [
333 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1683338.5.jpg",
334 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1683999.304.jpg",
335 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1685749.234.jpg",
336 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1686815.858.jpg",
337 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1686882.522.jpg",
338 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1688649.118.jpg",
339 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1689749.074.jpg",
340 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1691798.992.jpg",
341 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1693432.26.jpg",
342 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1693732.248.jpg",
343 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694015.57.jpg",
344 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694032.236.jpg",
345 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694098.9.jpg",
346 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694315.558.jpg",
347 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694498.884.jpg",
348 "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/1694815.538.jpg"
349 ],
350 "video": "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/screencast.mp4",
351 "gif": "https://calibre-screenshots-prod.s3.amazonaws.com/67ec59cf-0121-4ab7-ab38-e2f4459c40da/video-timeline/screencast.gif"
352 },
353 "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"
354 }
355 }
356 ]
357}
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}

Security & Verification

Webhooks can be securely validated using the Calibre-HMAC-SHA256-Signature header. This header should be used in order to establish that the incoming request originates from Calibre.

This header is formulated by supplying a shared secret with Calibre.

Setting a shared secret

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

Verifing the secret

Calibre uses HMAC cryptographic hash signature for verification purposes.

In order to verify the Calibre request, you will need to use the shared secret that you supplied earlier.

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

Notes:

  • In the examples above we have set the original secret as an environment variable. Never commit the secret to source control.
  • Comparing hashes using the equal comparison operator (==) is not advised as it cannot protect against timing attacks.

We recommend that you use a third party webhook test service, or enable verbose logging in order to verify the webhooks are delivered in the format as expected.