Keyword: async screenshot API ยท Updated July 30, 2026

Async Screenshot API with Webhooks: Queue Long Captures

Some screenshot jobs should not hold an HTTP client open while Chromium renders a page. A product preview queue, report generator, marketplace audit, or review workflow usually needs a job id immediately, then a callback when the image is ready. SnapshotFlow supports that pattern with async=true, signed webhooks, and a polling fallback.

Why revisit async screenshot workflows now?

Webhook reliability is getting fresh attention because more teams are wiring APIs into automation, agents, and internal queues. GitHub's current REST API guidance tells integrators to prefer webhooks over polling where possible and to use queues to avoid concurrency pressure. Stripe's webhook docs warn that duplicate events can occur and recommend asynchronous processing for handlers that may see delivery spikes.

Those are not screenshot-specific claims, but they are directly relevant to screenshot capture. A browser render is heavier and less predictable than a metadata lookup. If your caller only needs to register the work, send it to an async endpoint and process the completion event separately.

Scope: this guide is about one public URL per async job. If you need up to 10 URLs with the same settings in one request, use the batch screenshot API guide. If you need a simple synchronous capture, start with the Screenshot API quick start.

When async mode is the better fit

Use async whenUse a normal screenshot request when
The caller should continue immediately after enqueueing a capture.The user is waiting for a preview image in the current screen.
Your workflow already has a webhook receiver, queue, or background worker.You are running a local script and can save the binary response directly.
You need a stable job_id for audit logs, retry checks, or support tickets.You only need one PNG, JPEG, WebP, or PDF now.
A missed callback can be recovered by polling GET /jobs/:id.You cannot expose a public HTTPS webhook endpoint.

1. Submit the screenshot job

Call GET /screenshot with async=true. In production, webhook_url must be a public HTTPS URL; SnapshotFlow validates webhook URLs with the same private-network protection used for screenshot targets.

curl --get "https://api.snapshotflow.com/screenshot" \ -H "X-Api-Key: $SNAPSHOTFLOW_API_KEY" \ --data-urlencode "url=https://example.com/pricing" \ --data-urlencode "async=true" \ --data-urlencode "webhook_url=https://yourapp.com/webhooks/snapshotflow" \ --data-urlencode "external_identifier=pricing-audit-2026-07-30" \ --data-urlencode "format=png" \ --data-urlencode "width=1440" \ --data-urlencode "height=900" \ --data-urlencode "full_page=true" \ --data-urlencode "wait_until=networkidle2"

The API returns 202 Accepted with the job id and current status. If you supplied external_identifier, SnapshotFlow echoes it in the response so your app can confirm the correlation value without parsing the submitted URL.

{ "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "external_identifier": "pricing-audit-2026-07-30" }

2. Receive and verify the webhook

When the capture finishes, SnapshotFlow sends a JSON POST to your webhook URL. Authenticated accounts can create a webhook signing secret; when a secret exists, each delivery includes X-SnapshotFlow-Signature in the format t=<unix-seconds>,sha256=<hex>. Verify it against the raw request body before trusting the payload.

POST /webhooks/snapshotflow Content-Type: application/json User-Agent: SnapshotFlow-Webhook/1.0 (+https://snapshotflow.com/docs/webhooks) X-SnapshotFlow-Reference: 550e8400-e29b-41d4-a716-446655440000 X-SnapshotFlow-Attempt: 1 X-SnapshotFlow-External-Id: pricing-audit-2026-07-30 X-SnapshotFlow-Signature: t=1785360000,sha256=... { "event": "screenshot.completed", "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "done", "external_identifier": "pricing-audit-2026-07-30", "result": { "storagePath": "https://storage.example.com/signed-download-url", "url": "https://example.com/pricing", "format": "png", "width": 1440, "height": 900, "sizeBytes": 184221 }, "meta": { "rendering_ms": 2430, "size_bytes": 184221, "completed_at": "2026-07-30T14:00:00.000Z" } }

Minimal Express receiver

This example uses the official Node SDK helper. The important parts are raw-body verification, job_id deduplication, and returning 2xx only after your app has accepted the event.

import express from "express"; import { SnapshotFlow } from "snapshotflow"; const app = express(); const seenJobs = new Set(); app.post( "/webhooks/snapshotflow", express.raw({ type: "application/json" }), async (req, res) => { const signatureHeader = req.header("x-snapshotflow-signature"); if (!signatureHeader) return res.status(401).send("missing signature"); const rawBody = req.body.toString("utf8"); const ok = SnapshotFlow.verifyWebhook({ rawBody, signatureHeader, secret: process.env.SNAPSHOTFLOW_WEBHOOK_SECRET, toleranceSec: 300, }); if (!ok) return res.status(401).send("bad signature"); const payload = JSON.parse(rawBody); if (seenJobs.has(payload.job_id)) return res.sendStatus(200); seenJobs.add(payload.job_id); // Queue durable work here: download result.storagePath, update your DB, // notify a review channel, or mark the capture failed. await recordSnapshotFlowEvent(payload); return res.sendStatus(200); } );
Do not parse JSON before signature verification. Re-serializing JSON can change whitespace or key order and break HMAC validation. This is a common webhook implementation issue documented by major webhook providers.

3. Use the Node SDK when you prefer typed calls

The SnapshotFlow Node SDK maps camelCase options to the API's snake_case query parameters, starts async jobs with takeAsync(), checks status with getJob(), and can block until completion with waitForJob().

import { SnapshotFlow } from "snapshotflow"; const client = new SnapshotFlow({ apiKey: process.env.SNAPSHOTFLOW_API_KEY, }); const { jobId, status } = await client.takeAsync({ url: "https://example.com/pricing", webhookUrl: "https://yourapp.com/webhooks/snapshotflow", externalIdentifier: "pricing-audit-2026-07-30", fullPage: true, width: 1440, height: 900, }); console.log(jobId, status); // pending // Fallback path when the webhook is delayed or your receiver was down. const done = await client.waitForJob(jobId, { intervalMs: 1000, timeoutMs: 120000, }); if (done.status === "done") { console.log(done.result?.storagePath); }

Operational advice and edge cases

  • Keep a polling fallback. SnapshotFlow jobs are stored in memory and expire after one hour. Webhook delivery is best-effort with one retry for transient failures, so poll GET /jobs/:id if a missed callback would block downstream work.
  • Deduplicate by SnapshotFlow's job id. Use job_id or X-SnapshotFlow-Reference as the idempotency key. Use external_identifier to connect the capture to your own record, such as a report id or queue id.
  • Treat signed URLs as handoff URLs. The webhook payload includes result.storagePath. Copy the file to your durable storage if your business process needs long-term evidence.
  • Classify failures correctly. A failed capture webhook can include error.code values such as QUOTA_EXCEEDED, BLOCKED, TIMEOUT, INVALID_URL, or NAVIGATION_FAILED. If you set webhook_errors=false, SnapshotFlow omits the error block from failure payloads.
  • Use final HTTPS URLs. In production, webhook URLs must use HTTPS. Avoid redirects and placeholder endpoints; webhooks should hit the exact receiver path your app owns.
  • Avoid credential leakage. Send the SnapshotFlow API key in X-Api-Key. Keep webhook secrets in your server-side secret manager, not in a browser or client-side workflow.

FAQ

Does async mode make screenshots faster?

It makes the caller return faster. The browser still has to render the page, wait for the configured load condition, capture the output, and save the result before the webhook fires.

Can I use async mode without a webhook?

Yes. Submit async=true without webhook_url, keep the returned job_id, and poll GET /jobs/:id. This is useful for agents and backend jobs that already have a polling loop.

What happens if the webhook receiver returns 500?

SnapshotFlow treats 408, 429, 5xx, network errors, and delivery timeouts as retryable and makes one delayed retry. Other 3xx and 4xx responses are treated as permanent receiver-side failures, so fix the endpoint and use the job polling fallback if needed.

Sources and further reading

Build the callback path before scaling captures

Start with one URL, one webhook receiver, raw-body verification, and a polling fallback. Add queues, storage, and notifications after that path is observable.

Read async API docs