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
| Header | Meaning |
|---|---|
Webhook-Id | Stable delivery id. Use it to dedupe retries. |
Webhook-Timestamp | Unix seconds at signing time. |
Webhook-Signature | t=<ts>,v1=<hex hmac sha256>. Signed payload is ${ts}.${rawBody}. |
X-DeepSyte-Event | The event type, mirrors type in the body. |
User-Agent | DeepSyte-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
2xxto mark the delivery successful. Anything else (or a network failure / 15s timeout) schedules the next attempt. - After the final failure the delivery is marked
exhaustedand visible inGET /v1/webhooks/:id/deliveries. - We do not guarantee strict ordering. Treat events as idempotent via the
Webhook-Idheader.
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.
