Project Webhooks
Pixel Eagle can notify your service when a comparison finishes. Configure a webhook URL (and an optional signing secret) in your project settings — every comparison run for that project will then POST a JSON payload to that URL.
Configuration
Open your project's Settings page and set:
Webhook URL— must start withhttp://orhttps://.Webhook secret— optional. When set, every request is signed and the signature is sent in theX-PixelEagle-Signatureheader.
Webhooks require an active subscription on the space.
Delivery
Each delivery is an HTTP POST with these headers:
Content-Type: application/jsonX-PixelEagle-Event: comparison.completedX-PixelEagle-Signature: sha256=<hex>— only when a secret is configured.User-Agent: PixelEagle-Webhook/1
Each request times out after 10 seconds. Pixel Eagle retries up to 3 times with backoff (1s, then 4s) on connection errors, 5xx, and 429. Other non-2xx responses are treated as permanent failures and are not retried.
Endpoints should respond with any 2xx status as quickly as possible and process the payload asynchronously.
Event: comparison.completed
Sent once per comparison, whether it succeeded or failed. Successful payload:
{
"event": "comparison.completed",
"comparison_id": 12345,
"project_id": "0c6b8b9e-1f3a-4d2c-9a51-8d6f6b2f7e10",
"status": "done",
"from_run": 482,
"to_run": 491,
"counts": {
"diff": 3,
"new": 1,
"missing": 0
},
"settings": {
"flip": true,
"tolerance": 0.05,
"threshold": 0.1,
"dilate": 0
},
"links": {
"ui": "https://pixel-eagle.com/p/<project_id>/compare/482/491",
"api": "https://pixel-eagle.com/runs/482/compare/491"
},
"completed_at": "2026-04-26T10:42:18Z"
} On failure, status is "failed", counts is omitted, and an error field describes what went wrong:
{
"event": "comparison.completed",
"comparison_id": 12346,
"project_id": "0c6b8b9e-1f3a-4d2c-9a51-8d6f6b2f7e10",
"status": "failed",
"error": "image dimensions could not be reconciled",
"from_run": 482,
"to_run": 491,
"settings": {
"flip": true,
"tolerance": 0.05,
"threshold": 0.1,
"dilate": 0
},
"links": {
"ui": "https://pixel-eagle.com/p/<project_id>/compare/482/491",
"api": "https://pixel-eagle.com/runs/482/compare/491"
},
"completed_at": "2026-04-26T10:42:21Z"
}Field reference:
event— alwayscomparison.completedfor now.comparison_id— numeric id of the comparison.project_id— UUID of the project the comparison belongs to.status—doneorfailed.error— only present whenstatusisfailed.from_run,to_run— the two run ids that were compared.counts— number ofdiff,new, andmissingscreenshots. Omitted on failure.settings— the comparison settings used (flip,tolerance,threshold,dilate).links.ui/links.api— direct links to the comparison.completed_at— RFC 3339 / ISO 8601 timestamp in UTC.
Verifying the signature
When a secret is configured, the request body is signed with HMAC-SHA256 using that secret. The signature is sent as a hex digest in the X-PixelEagle-Signature header, prefixed with sha256=:
X-PixelEagle-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Compute HMAC-SHA256 over the raw request body using your secret as the key, hex-encode the result, and compare it against the value after sha256= using a constant-time comparison. Reject the request if they differ.
Node.js
import crypto from 'node:crypto'
function verify(rawBody, header, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
const provided = header.replace(/^sha256=/, '')
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(provided, 'hex'),
)
}Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
provided = header.removeprefix("sha256=")
return hmac.compare_digest(expected, provided) If you do not configure a secret, the X-PixelEagle-Signature header is not sent and you cannot verify the origin of the request — only use unsigned webhooks for local testing.