Guides / Async & webhooks
Async jobs and webhooks
Long-running captures should not hold your HTTP client open. Background mode returns a job identifier immediately, uploads the finished asset to private storage, and notifies your server with a signed URL you can fetch or forward to clients.
Overview
Flip background: true and include a publicly reachable callback_url (HTTPS recommended). Quota is consumed when the job is accepted by the async pipeline. If the job cannot be persisted or the worker rejects the enqueue request, the consumed quota is released.
Enqueue a render
const res = await fetch("https://screennabster.com/api/v1/capture", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.SCREENNABSTER_API_KEY!,
},
body: JSON.stringify({
url: "https://example.com/long-report",
output: "pdf",
background: true,
callback_url: "https://api.mycompany.com/hooks/screennabster",
webhook_errors: true,
external_identifier: "report-2026-04-25",
}),
});
const body = await res.json();
// { job_id, status: "queued", message: "…" }What your server receives
We POST JSON to callback_url with header X-ScreenNabster-Signature: sha256=<hex>. The body is the raw string we signed—verify before trusting fields. If you pass external_identifier, we include it in the body and as X-ScreenNabster-External-Identifier.
{
"event": "capture.completed",
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2026-04-11T15:04:05.123Z",
"result_url": "https://…supabase…/signed-url…",
"external_identifier": "report-2026-04-25"
}{
"event": "capture.failed",
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2026-04-11T15:04:05.123Z",
"error": "Navigation timeout",
"error_code": "RENDER_ERROR",
"external_identifier": "report-2026-04-25"
}result_url is a time-limited signed URL (on the order of days). Download or copy into your storage promptly; do not treat it as a permanent CDN link.
Verify signatures
Compute HMAC-SHA256 over the raw request body with the same secret configured in our environment (SCREENNABSTER_WEBHOOK_SIGNING_SECRET). Compare to the header value using a timing-safe equality function. Example skeleton:
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody: string, header: string | null, secret: string) {
if (!header?.startsWith("sha256=")) return false;
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}Failures & retries
Worker errors and storage upload failures produce capture.failed events only when webhook_errors is true, with an explanatory error string and an error_code such as RENDER_ERROR or STORAGE_ERROR.
Webhook delivery is attempted once. A 2xx response marks the callback as sent; non-2xx responses and network failures increment the attempt counter but are not retried automatically yet. Return 200 quickly and make handlers idempotent.
Operational tips
- Return
200quickly and offload work to a queue—our sender does not need your business logic latency. - Log
job_idto correlate with dashboard usage rows. - Combine with tips in the performance guide to keep worker time predictable.