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 with http:// or https://.
  • Webhook secret — optional. When set, every request is signed and the signature is sent in the X-PixelEagle-Signature header.

Webhooks require an active subscription on the space.

Delivery

Each delivery is an HTTP POST with these headers:

  • Content-Type: application/json
  • X-PixelEagle-Event: comparison.completed
  • X-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 — always comparison.completed for now.
  • comparison_id — numeric id of the comparison.
  • project_id — UUID of the project the comparison belongs to.
  • statusdone or failed.
  • error — only present when status is failed.
  • from_run, to_run — the two run ids that were compared.
  • counts — number of diff, new, and missing screenshots. 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.