PDFShot

Webhooks

Stop polling. Get notified the moment a PDF is ready, with HMAC-signed payloads and automatic retries.

How it works

Subscribe an HTTPS endpoint via the DeepSyte webhook API (one subscription covers PDFShot too — same account, same endpoints):

curl -X POST https://api.deepsyte.com/v1/webhooks \
  -H "Authorization: Bearer $DEEPSYTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/pdfshot",
    "events": ["screenshot.completed", "screenshot.failed"]
  }'

The response includes a signing secret (returned once — store it):

{
  "endpoint": {
    "id": "we_abc123",
    "url": "https://example.com/hooks/pdfshot",
    "events": ["screenshot.completed", "screenshot.failed"],
    "secret": "whsec_..."
  }
}

Filter to PDFs

PDFShot conversions emit screenshot.completed with format: "pdf". If you want PDF callbacks only, subscribe to the full event but filter server-side:

app.post("/hooks/pdfshot", express.raw({ type: "*/*" }), (req, res) => {
  // ... verify signature, parse body ...
  if (event.type === "screenshot.completed" && event.data.format === "pdf") {
    await db.pdfs.update(event.data.screenshotId, {
      url: event.data.publicUrl,
    });
  }
  res.status(200).json({ ok: true });
});

Payload shape

{
  "type": "screenshot.completed",
  "createdAt": "2026-05-28T03:42:09.211Z",
  "data": {
    "screenshotId": "wJqTW...",
    "url": "https://example.com",
    "publicUrl": "https://pub-...r2.dev/screenshots/wJqTW....pdf",
    "format": "pdf",
    "width": 1280,
    "height": null
  }
}

Headers

HeaderMeaning
Webhook-IdStable delivery id. Use it to dedupe retries.
Webhook-TimestampUnix seconds at signing time.
Webhook-Signaturet=<ts>,v1=<hex hmac sha256>. Signed payload is ${ts}.${rawBody}.
X-DeepSyte-EventThe event type, mirrors type in the body.
User-AgentDeepSyte-Webhook/1

Reject any request older than 5 minutes (clock-skew tolerance). After verifying the signature, treat the body's type as authoritative.

Verify the signature

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=", 2) as [string, string]),
  );
  const ts = Number(parts.t);
  if (!ts || Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  const sig = Buffer.from(parts.v1 ?? "", "hex");
  const exp = Buffer.from(expected, "hex");
  return sig.length === exp.length && timingSafeEqual(sig, exp);
}

Retries and ordering

  • Each delivery attempts up to 6 times with exponential backoff: 1m → 5m → 30m → 2h → 12h (~14 hours total).
  • Respond with any 2xx to mark the delivery successful. Anything else (or a network failure / 15s timeout) schedules the next attempt.
  • After the final failure the delivery is marked exhausted and visible in GET /v1/webhooks/:id/deliveries.
  • We do not guarantee strict ordering. Treat events as idempotent via the Webhook-Id header.

Manage endpoints

Full CRUD on the same /v1/webhooks surface — see deepsyte.com/docs/api/webhooks for the rotate / test / list-deliveries / delete endpoints. They work for every DeepSyte event type, not just PDF ones.

On this page