TutorialWebhooksAsync
Screenshot API Webhooks: Real-Time Notifications Tutorial
Published March 14, 2026 -- 10 min read
Synchronous screenshot capture works for simple use cases, but when you need to process hundreds of URLs or capture slow-loading pages, async processing with webhooks is the way to go. This tutorial shows you how.
Why Async + Webhooks?
- No timeout limits: Synchronous requests must complete in 30-60 seconds. Async jobs can run for minutes.
- Batch processing: Submit 100 URLs at once, get notified as each completes.
- Better error handling: Retry failed captures automatically without blocking your main thread.
- Scalability: Your app stays responsive while screenshots render in the background.
How It Works
- Submit async request: POST to
/v1/screenshot/asyncwith acallback_url - Get job ID immediately: The API returns a job ID and status URL within milliseconds
- Screenshot renders in background: The API captures the page asynchronously
- Webhook fires on completion: Your callback URL receives the screenshot data
Step 1: Submit an Async Request
curl -X POST https://screenshotapi-api-production.up.railway.app/v1/screenshot/async \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://stripe.com",
"width": 1280,
"height": 800,
"format": "png",
"device": "desktop",
"callback_url": "https://your-app.com/webhook/screenshot"
}'
# Response (immediate):
{
"id": "job_abc123",
"status": "pending",
"status_url": "/v1/status/job_abc123",
"message": "Screenshot job queued. Poll the status_url or wait for webhook callback."
}Step 2: Set Up Your Webhook Endpoint
Create an endpoint that receives POST requests with the screenshot data. Here is a Node.js/Express example:
import express from 'express';
import fs from 'fs';
const app = express();
app.use(express.json({ limit: '50mb' })); // Screenshots can be large
app.post('/webhook/screenshot', (req, res) => {
const { id, status, content_type, size_bytes, duration_ms, data_base64, error } = req.body;
if (status === 'completed') {
console.log(`Screenshot ${id} completed in ${duration_ms}ms (${size_bytes} bytes)`);
// Save the screenshot
const buffer = Buffer.from(data_base64, 'base64');
const ext = content_type.split('/')[1]; // png, jpeg, webp
fs.writeFileSync(`screenshots/${id}.${ext}`, buffer);
// Update your database, send notification, etc.
// updateScreenshotRecord(id, { status: 'completed', size: size_bytes });
}
if (status === 'failed') {
console.error(`Screenshot ${id} failed: ${error}`);
// Handle failure: retry, notify admin, etc.
}
// Always respond 200 to acknowledge receipt
res.sendStatus(200);
});
app.listen(3000);Step 3: Poll Status (Alternative to Webhooks)
If you cannot set up a webhook endpoint, you can poll the status URL:
async function waitForScreenshot(jobId) {
const API_BASE = 'https://screenshotapi-api-production.up.railway.app';
while (true) {
const res = await fetch(`${API_BASE}/v1/status/${jobId}`, {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});
const data = await res.json();
if (data.status === 'completed') {
// Download the completed screenshot
const imgRes = await fetch(`${API_BASE}/v1/status/${jobId}/download`, {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});
return Buffer.from(await imgRes.arrayBuffer());
}
if (data.status === 'failed') {
throw new Error(`Screenshot failed: ${data.error}`);
}
// Wait 2 seconds before checking again
await new Promise(resolve => setTimeout(resolve, 2000));
}
}Webhook Payload Reference
Success Payload
{
"id": "job_abc123",
"status": "completed",
"content_type": "image/png",
"size_bytes": 245760,
"duration_ms": 4523,
"data_base64": "iVBORw0KGgoAAAANSUhEUg..."
}Failure Payload
{
"id": "job_abc123",
"status": "failed",
"error": "Page load timed out"
}Batch Processing Pattern
Process hundreds of URLs efficiently by submitting async requests in bulk:
const urls = [
'https://stripe.com',
'https://github.com',
'https://vercel.com',
// ... hundreds more
];
const API_BASE = 'https://screenshotapi-api-production.up.railway.app';
// Submit all jobs
const jobs = await Promise.all(urls.map(async (url) => {
const res = await fetch(`${API_BASE}/v1/screenshot/async`, {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
format: 'png',
device: 'desktop',
callback_url: 'https://your-app.com/webhook/screenshot',
}),
});
return res.json();
}));
console.log(`Submitted ${jobs.length} screenshot jobs`);
// All results arrive via webhook -- no polling neededBest Practices
- Verify webhook signatures: In production, verify that webhook requests actually come from ScreenshotAPI
- Respond quickly: Your webhook endpoint should respond within 5 seconds. Process data asynchronously if needed.
- Handle retries: Webhooks may be sent more than once. Use the job ID to deduplicate.
- Store data immediately: Save the base64 data to disk or cloud storage right away. Do not keep it in memory.
- Rate limit your submissions: When doing batch processing, submit 10-50 jobs at a time, not all at once.
Try async screenshots
Async processing with webhooks is available on all plans. Get started with 100 free screenshots per month.