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, before the worker starts, matching synchronous behavior so billing stays predictable.

Enqueue a render

enqueue.ts
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",
  }),
});
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.

completed.json
{
  "event": "capture.completed",
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2026-04-11T15:04:05.123Z",
  "result_url": "https://…supabase…/signed-url…"
}
failed.json
{
  "event": "capture.failed",
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2026-04-11T15:04:05.123Z",
  "error": "Navigation timeout"
}

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:

verify.ts
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 produce capture.failed events with an explanatory error string. Network issues delivering the webhook may prevent your system from hearing about success—design idempotent handlers and consider polling internal job status if we expose it in a future API revision.

Operational tips

  • Return 200 quickly and offload work to a queue—our sender does not need your business logic latency.
  • Log job_id to correlate with dashboard usage rows.
  • Combine with tips in the performance guide to keep worker time predictable.