Skip to content

Core concepts

Webhooks

Receive signed notifications when async jobs finish.

With is_async=true and a webhook_url, ScreenshotDude sends the finished job to your server — no polling needed. Webhooks are available on paid plans.

curl -G "http://localhost:4000/v1/capture" \
  --data-urlencode "url=https://example.com" \
  -d full_page=true -d is_async=true \
  --data-urlencode "webhook_url=https://your-app.example/hooks/snaplens" \
  -H "x-api-key: $SNAPLENS_API_KEY"

Delivery

When the job reaches a terminal status (CompletedJob, Error or BadRequest) we send:

  • Method: POST
  • Content-Type: application/json
  • Body: the job object
  • Header: x-snaplens-signature: t=<unix timestamp>,v1=<signature>

Respond with any 2xx status within 10 seconds. Do heavy work after responding (queue it).

Verifying signatures

Each workspace has a webhook secret (dashboard → Settings → Account). The signature is:

v1 = hex( HMAC_SHA256( secret, "<t>.<raw request body>" ) )

To verify:

  1. Parse t and v1 from the x-snaplens-signature header.
  2. Compute the HMAC over the raw body bytes, exactly as received — don't re-serialize parsed JSON.
  3. Compare with v1 using a constant-time comparison.
  4. Reject timestamps older than 5 minutes to prevent replays.
import { createHmac, timingSafeEqual } from "node:crypto";

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

// Next.js route handler example
export async function POST(req: Request) {
  const raw = await req.text();
  if (!verifySnaplensSignature(raw, req.headers.get("x-snaplens-signature"), process.env.SNAPLENS_WEBHOOK_SECRET!)) {
    return new Response("invalid signature", { status: 401 });
  }
  const job = JSON.parse(raw);
  // enqueue job.result for processing…
  return new Response("ok");
}

The Node SDK also ships SnaplensClient.verifyWebhook(secret, rawBody, header).

Retries

If your endpoint doesn't return 2xx (or times out), we retry with exponential backoff — several attempts over roughly an hour. Your handler should be idempotent: use the job id to ignore duplicates.

If all retries fail, the result is still available via GET /v1/jobs/{id} until it expires.

Security tips

  • Always verify the signature; the URL alone is not a secret.
  • Use HTTPS endpoints.
  • Rotate the webhook secret from the dashboard if it leaks (verify against both secrets during the switch).