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:
- Parse
tandv1from thex-snaplens-signatureheader. - Compute the HMAC over the raw body bytes, exactly as received — don't re-serialize parsed JSON.
- Compare with
v1using a constant-time comparison. - 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).