Screenshot API Webhook
A regular screenshot request is straightforward: you send a URL, wait, and get an image back. For simple sites this takes a couple of seconds and works great. The trouble starts when the site is heavy — a dashboard with charts, a single-page app, a page loading resources from a dozen external domains. Pages like these can take 15-30 seconds to render, and the whole time your connection stays open. If your server or Lambda times out before the image is ready, the request just dies, but the credit is already spent. The screenshot API webhook parameter fixes this: add webhook_url to your request, get an instant 202 Accepted response, and receive the finished screenshot as a separate POST to your server whenever it's ready. No hanging connections, no timeouts, no lost credits.
Why synchronous screenshot requests break in production
As long as you're working with simple marketing pages, synchronous requests do fine — the response comes back in a couple of seconds. But the moment something dynamic enters the picture, things start falling apart.
Take a React dashboard. First an empty shell loads, then it pulls data from an API and draws the charts. That whole process can take 8-12 seconds. During all that time your server is just sitting there, holding an open HTTP connection, doing nothing useful. One request like that is no big deal. But if you need to process hundreds of URLs, every thread on your server ends up parked waiting for screenshots, and your main application starts lagging for regular users.
Serverless makes it even trickier. Lambda has a default timeout of 15 seconds. Cloudflare Workers give you 30 seconds on paid plans. If the screenshot isn't done in time, the function simply terminates and you're left with nothing. You either have to drop serverless for this task entirely or build a complex architecture with Step Functions and queues.
There's also the cost angle. Every request that times out still burns a credit but returns no image. I've seen setups where 15-20% of screenshots were lost to timeouts. All billable. Webhooks eliminate this problem completely.
How does a screenshot API webhook work?
Think of it as a screenshot API callback — instead of waiting for the image in the response, you tell the API where to send it once it's ready. The flow is simple: you send a request with a webhook_url parameter, the API queues the job and immediately responds with 202 Accepted plus the screenshot ID. The entire exchange takes less than 200 milliseconds, and your code moves on.
Behind the scenes, the renderer spins up a browser, opens the page, waits for it to load (respecting your delay, selector, and timeout settings), captures the screenshot, and saves the file. As soon as the image is ready, a POST request hits your webhook_url with JSON: screenshot metadata, dimensions, file size, processing time, and a download link.
If something goes wrong (DNS doesn't resolve, timeout, page blocked), you still get a webhook — but with a screenshot.failed event and an error code. No silent failures, no endless polling. This async screenshot API pattern works the same way across all output formats: PNG, JPEG, WebP, PDF.
Send a screenshot request with webhook_url
Just add webhook_url to a regular request. Every other parameter works as before — full page, dark mode, custom viewport, PDF export. The only difference is that the result arrives as a POST to your endpoint instead of in the response body.
curl -X POST "https://api.screenshotrun.com/v1/screenshots/capture" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example.com/dashboard",
"webhook_url": "https://your-server.com/webhooks/screenshots",
"full_page": true,
"format": "png",
"delay": 3
}'
The response comes back instantly:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"url": "https://app.example.com/dashboard",
"webhook_url": "https://your-server.com/webhooks/screenshots",
"created_at": "2026-09-09T14:30:00.000000Z"
}
Your code keeps running. Nothing waits.
// Node.js — fire and forget
const response = await fetch('https://api.screenshotrun.com/v1/screenshots/capture', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://app.example.com/dashboard',
webhook_url: 'https://your-server.com/webhooks/screenshots',
full_page: true,
format: 'png',
delay: 3,
}),
});
const { id, status } = await response.json();
console.log(`Screenshot ${id} queued (${status})`);
// Move on — result will arrive at your webhook endpoint
# Python — queue the screenshot and move on
import requests
resp = requests.post(
"https://api.screenshotrun.com/v1/screenshots/capture",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"url": "https://app.example.com/dashboard",
"webhook_url": "https://your-server.com/webhooks/screenshots",
"full_page": True,
"format": "png",
"delay": 3,
},
)
data = resp.json()
print(f"Screenshot {data['id']} queued ({data['status']})")
# Result will arrive at your webhook endpoint
What does the screenshot webhook payload contain?
When the screenshot is ready, your endpoint receives a POST request with JSON. There are two event types: screenshot.completed for successful captures and screenshot.failed for errors.
A successful delivery looks like this:
{
"event": "screenshot.completed",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"url": "https://app.example.com/dashboard",
"options": {
"width": 1280,
"height": 800,
"format": "png",
"full_page": true,
"delay": 3
},
"file_size": 245760,
"mime_type": "image/png",
"width": 1280,
"height": 4200,
"processing_time_ms": 5430,
"image_url": "https://api.screenshotrun.com/v1/screenshots/550e8400-.../image",
"created_at": "2026-09-09T14:30:00.000000Z",
"completed_at": "2026-09-09T14:30:05.430000Z"
},
"sent_at": "2026-09-09T14:30:05.600000Z"
}
To download via image_url you need the API key in the Authorization header. The processing_time_ms field tells you how long the capture took, which is handy for monitoring slow pages. The options object returns all the parameters from your original request, so your webhook handler knows exactly which settings produced this particular screenshot.
Failed captures arrive with a different event and an error code:
{
"event": "screenshot.failed",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"url": "https://app.example.com/dashboard",
"error": {
"message": "Page load timed out after 30 seconds.",
"code": "TIMEOUT"
},
"created_at": "2026-09-09T14:30:00.000000Z"
},
"sent_at": "2026-09-09T14:30:35.000000Z"
}
Error codes include TIMEOUT, DNS_FAILED, CONNECTION_FAILED, BLOCKED, RENDER_ERROR, and JOB_FAILED. Each code tells you what went wrong and whether it makes sense to retry.
Sync vs async: when to use which
You don't always need a webhook. For quick captures of simple pages, synchronous mode is easier and more convenient. The non-blocking screenshot API approach pays off when page complexity or URL volume starts growing.
| Factor | Synchronous | Async with webhook |
|---|---|---|
| Response time | 1-30 seconds (blocks until done) | Under 200ms (202 Accepted) |
| Timeout risk | High for complex pages | None — processing happens in background |
| Batch processing | Requires thread pool management | Fire all requests, results arrive as ready |
| Serverless compatible | Only if page renders within function timeout | Fully compatible with Lambda, Workers, Edge |
| Error handling | HTTP error in response | Error event delivered to webhook |
| Best for | Single screenshots, simple pages, real-time previews | Batch jobs, dashboards, SPAs, monitoring, CI/CD |
A simple rule of thumb: if the page loads in under 3 seconds and you have fewer than ten URLs, a synchronous request is easier. Once volumes grow, webhooks save you from building your own retry logic and managing connection pools.
How do I process hundreds of screenshots with webhooks?
A batch screenshot webhook turns processing hundreds of URLs into a simple task. Send 500 requests with the same webhook_url, and results arrive one by one as each screenshot finishes. No need to build a queue, track statuses, or poll the API.
// Node.js — batch 100 URLs with webhooks
const urls = ['https://example1.com', 'https://example2.com', /* ... */];
const promises = urls.map(url =>
fetch('https://api.screenshotrun.com/v1/screenshots/capture', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
webhook_url: 'https://your-server.com/webhooks/screenshots',
format: 'png',
}),
})
);
const results = await Promise.all(promises);
console.log(`${results.length} screenshots queued`);
// Results will arrive at your webhook endpoint over the next few minutes
Each result arrives separately.
To track progress, count the incoming webhooks or match them to requests by the screenshot id. For very large volumes (thousands of URLs), add a small delay between requests to stay within the rate limit. The API documentation describes rate limit headers for dynamic throttling.
How do I verify that a webhook request is authentic?
Webhook signature verification protects your handler from forged requests. For user-level webhooks (configured in the dashboard) ScreenshotRun supports HMAC-SHA256 signatures. When you create an endpoint, the system generates a secret key with a whsec_ prefix. Every delivery includes an X-Webhook-Signature header containing the HMAC signature of the JSON payload.
Verification in Node.js:
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your Express handler
app.post('/webhooks/screenshots', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the screenshot...
res.status(200).send('OK');
});
Per-request webhooks (sent via webhook_url directly in the request) don't include a signature. For those, you can verify authenticity by checking the screenshot ID, adding a secret token to your URL path, or restricting the endpoint to ScreenshotRun's IP addresses.
What happens if my webhook endpoint is down?
Delivery retries automatically with increasing intervals. One initial attempt plus three retries:
| Attempt | Delay after failure | Total elapsed |
|---|---|---|
| 1st (initial) | Immediate | ~0 seconds |
| 2nd (retry) | 10 seconds | ~10 seconds |
| 3rd (retry) | 60 seconds | ~70 seconds |
| 4th (final retry) | 5 minutes | ~6 minutes |
A delivery counts as successful on any 2xx response from your server. Everything else (4xx, 5xx, a 10-second connection timeout, DNS error) triggers a retry. Each attempt is logged in the dashboard with the response status and body from your server. Useful for debugging when you can't figure out why a delivery keeps failing.
If all attempts are exhausted, the webhook is marked as failed. The screenshot itself doesn't go anywhere though. You can always fetch it via GET /v1/screenshots/{id} using the ID from the initial response. Nothing is lost — you just switch from push to pull.
Use cases that benefit most from async delivery
Five scenarios gain the most from a screenshot API webhook: scheduled monitoring, PDF generation, AI agents, visual testing in CI/CD, and bulk preview generation.
Website monitoring is the most obvious one. You need to capture hundreds of pages on a schedule and compare them to previous versions. If even one page loads slowly, a synchronous pipeline stalls. With webhooks each capture lives on its own. A slow page doesn't block the rest.
PDF generation from HTML templates — invoices, reports, documents with tables and charts. Rendering these pages takes time, and webhooks let your billing service simply queue the jobs and process finished PDFs as they arrive.
Then there are AI agents. Agents use screenshots as visual input: send a request, switch to other tasks, receive the image via webhook and process it. Pairs well with the MCP server integration.
Visual testing in CI/CD. Fire off screenshots in parallel during a build, each with a webhook_url pointing at a comparison service. The pipeline doesn't stall — it continues running unit tests while screenshots render in the background.
Preview generation for directories. Directories and catalogs need thumbnails for hundreds or thousands of URLs. Synchronous processing means hours of waiting. With webhooks you send all requests at once, previews arrive as they finish, and your catalog database updates in real time.
Async screenshots, one parameter
Get your API key — 200 free/monthThe self-hosted alternative
Building your own async pipeline is possible. Taking the screenshot is the easy part. The complexity starts with reliable result delivery: guaranteed dispatch with escalating retry intervals, handling dead endpoints, idempotency keys so you don't process the same screenshot twice, signature generation for authentication, delivery logging for debugging. Each of these is a separate weekend project.
It usually ends up as a Playwright worker, a Redis queue, and a growing collection of edge cases discovered in production. If you already have that kind of infrastructure for other tasks, adding screenshots to it makes sense. If not, building everything from scratch to save $9/month on an API is a tough trade-off. For a detailed comparison of the approaches: Puppeteer vs screenshot API and Playwright vs screenshot API.
Testing your webhook locally
During development your local server isn't reachable from the internet. Two approaches solve this.
Webhook testing services. Sites like webhook.site or requestbin.com give you a temporary public URL that logs incoming requests. Use it as your webhook_url to see the exact payload structure and headers.
Tunnels. ngrok, Cloudflare Tunnel, or localtunnel expose a local port to the internet. Point your webhook_url at the tunnel URL, and your local Express or FastAPI server receives the webhook directly. The best option for fully testing your handler logic.
Parameter reference
| Parameter | Type | Default | Description |
|---|---|---|---|
webhook_url | string (URL) | null | HTTPS endpoint where the completed screenshot will be POSTed. Max 2048 characters. Cannot point to private/internal IPs. |
The screenshot API webhook parameter is available on plans with webhook support. Full API reference is in the documentation. For permanent webhook endpoints that fire on every screenshot automatically, configure user-level webhooks in the dashboard.
Vitalii Holben