Webhooks

Background jobs and monitors can POST a JSON event to your https URL when something happens. Every request is signed, so you can check it came from Shotlane.

Events

EventSent whenSet with
render.finished / render.failedAn async render is done (failures only with webhook_errors=true).webhook_url with async=true
bulk.finishedA bulk job ends (also when some URLs failed).webhook_url on /v1/bulk
crawl.finishedA crawl ends.webhook_url on /v1/crawl
monitor.changedA monitored page changed past its threshold.webhook_url on a monitor
POST https://you.example/hooks/shotlane
Content-Type: application/json
X-Shotlane-Signature: sha256=5d2c…

{"event": "bulk.finished", "job": {"id": "…", "kind": "bulk", "status": "done", "total": 2, "ok": 2, "failed": 0,
         "status_url": "https://shotlane.dev/v1/bulk/…", "zip_url": "https://shotlane.dev/v1/bulk/…/zip", "results": [ … ]}}

{"event": "monitor.changed", "monitor": { … }, "check": {"id": 42, "score": 3.1, "summary": "…",
         "snapshot_url": "https://shotlane.dev/v1/monitors/…/checks/42/snapshot", "diff_url": "…"}}

Each request also carries X-Shotlane-Event and User-Agent: Shotlane-Webhook/1. A delivery is tried up to 3 times until your server answers with a 2xx (network errors and other statuses count as failures), 5 and 10 seconds apart. Answer quickly and do the work afterwards. Redirects aren't followed, and private network addresses are refused.

Verify the signature

X-Shotlane-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your API key. Compute it over the bytes you received, before parsing the JSON, and compare in constant time.

# Python (or: from shotlane import verify_webhook)
import hmac, hashlib
def verify(raw_body: bytes, header: str, api_key: str) -> bool:
    expected = "sha256=" + hmac.new(api_key.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")
// Node (or: import { verifyWebhook } from "./shotlane.mjs")
import crypto from "node:crypto";
function verify(rawBody, header, apiKey) {
  const expected = "sha256=" + crypto.createHmac("sha256", apiKey).update(rawBody).digest("hex");
  return header?.length === expected.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

After you rotate your API key, signatures use the new key.