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

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",
    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.

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…",
  "external_identifier": "report-2026-04-25"
}
failed.json
{
  "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:

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 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 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.