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#
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/jsonvia HTTP POST).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.
- Create the integration by clicking the Save button.

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/..."
}
}
]
}{
"id": "abc123",
"url": "http://calibreapp.com/teams/your-team/your-site/budgets/abc123",
"organisation_id": "your-organisation",
"site_id": "your-site",
"name": "Largest Contentful Paint",
"abbreviated_name": "LCP",
"value": 3000,
"formatted": "3s",
"status": "At risk",
"threshold": "GreaterThan",
"generated_at": "2020-09-30T05:33:17.970Z",
"budgets": [
{
"profile": {
"id": "pr123",
"name": "Chrome Desktop"
},
"page": {
"id": "p123",
"name": "Marketing Home",
"previews": []
},
"status": "At risk",
"value": 2850,
"formatted": "2.85s"
},
{
"profile": {
"id": "pr456",
"name": "MotoG4, 3G connection"
},
"page": {
"id": "p123",
"name": "Marketing Home",
"previews": []
},
"status": "Met",
"value": 2400,
"formatted": "2.4s"
},
{
"profile": {
"id": "pr789",
"name": "iPhone, 4G LTE"
},
"page": {
"id": "p123",
"name": "Marketing Home",
"previews": []
},
"status": "Met",
"value": 2400,
"formatted": "2.4s"
}
]
}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#
- Navigate to the selected webhook (Site → Synthetic → Settings → Integrations).
- Use the automatically generated secret, or create your own.
- 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')
})// SECRET_TOKEN is an environment variable
post '/calibre-webhook' do
request.body.rewind
payload_body = request.body.read
verify_signature(payload_body)
# Safely use the JSON
end
def verify_signature(payload_body)
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
return halt 500, "Signatures didn't match!" unless ActiveSupport::SecurityUtils.secure_compare(signature, request.env['Calibre-HMAC-SHA256-Signature'])
endIn 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.