Async Screenshot Processing with Webhooks: No More Timeouts
The Timeout Problem Synchronous screenshot APIs have a fundamental issue: HTTP timeouts. When you capture a complex page that takes 15-30 seconds to render, your HTTP connection can time out. Load balancers, proxies, and client libraries all enforce timeout limits. The solution? Asynchronous processing with webhooks. Submit the job, get a job ID, and receive a notification when it’s done. How Async Webhooks Work The flow is simple: Submit — Send a screenshot request with a webhookUrl Receive job ID — API returns immediately with a job identifier Processing — API captures the screenshot in the background Notification — API sends the result to your webhook URL Retrieve — Download the screenshot from the provided URL C A A l P P i I I e n → → t C W → l e i b A e t h P n i o I t m o : : e k : " " p C G a " a o s J p t s o t e b u i s r t a e , b c t j 1 h o 2 i b 3 s I i U D s R : L d , a o b n n c e o 1 , t 2 i 3 h f " e y r e m ' e s a t t h e w e s b c h r o e o e k n . s e h x o a t m p U l R e L . " c o m / h o o k " Submitting Async Requests Node.js const axios = require('axios'); async function submitScreenshotJob(url, options = {}) { const response = await axios.post( 'https://api.toolcenter.dev/v1/screenshot', { url: url, width: options.width || 1280, height: options.height || 800, format: options.format || 'png', fullPage: options.fullPage || false, webhookUrl: 'https://your-server.com/api/webhook/screenshot', }, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' }, } ); return response.data; // { jobId: 'abc123', status: 'queued' } } Python import requests def submit_screenshot_job(url, webhook_url): response = requests.post( 'https://api.toolcenter.dev/v1/screenshot', json={ 'url': url, 'width': 1280, 'height': 800, 'format': 'png', 'webhookUrl': webhook_url, }, headers={'Authorization': 'Bearer YOUR_API_KEY'} ) return response.json() # {'jobId': 'abc123', 'status': 'queued'} Building the Webhook Receiver Express.js const express = require('express'); const crypto = require('crypto'); const app = express(); app.use(express.json()); // Store for pending jobs const pendingJobs = new Map(); app.post('/api/webhook/screenshot', (req, res) => { const { jobId, status, screenshotUrl, error } = req.body; // Verify webhook signature const signature = req.headers['x-webhook-signature']; const expectedSig = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET) .update(JSON.stringify(req.body)) .digest('hex'); if (signature !== expectedSig) { return res.status(401).json({ error: 'Invalid signature' }); } console.log(`Job ${jobId}: ${status}`); if (status === 'completed') { // Download and process the screenshot processCompletedScreenshot(jobId, screenshotUrl); } else if (status === 'failed') { console.error(`Job ${jobId} failed: ${error}`); handleFailedJob(jobId, error); } // Always respond 200 to acknowledge receipt res.status(200).json({ received: true }); }); async function processCompletedScreenshot(jobId, screenshotUrl) { const response = await axios.get(screenshotUrl, { responseType: 'arraybuffer' }); const filename = `screenshots/${jobId}.png`; fs.writeFileSync(filename, response.data); console.log(`Saved: ${filename}`); // Resolve any pending promises const resolver = pendingJobs.get(jobId); if (resolver) { resolver.resolve(filename); pendingJobs.delete(jobId); } } app.listen(3000, () => console.log('Webhook server ready')); Flask (Python) from flask import Flask, request, jsonify import hmac import hashlib import os app = Flask(__name__) WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET'] @app.route('/api/webhook/screenshot', methods=['POST']) def handle_webhook(): # Verify signature signature = request.headers.get('X-Webhook-Signature', '') expected = hmac.new( WEBHOOK_SECRET.encode(), request.data, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): return jsonify({'error': 'Invalid signature'}), 401 data = request.json job_id = data['jobId'] status = data['status'] if status == 'completed': screenshot_url = data['screenshotUrl'] process_screenshot(job_id, screenshot_url) elif status == 'failed': handle_failure(job_id, data.get('error')) return jsonify({'received': True}), 200 def process_screenshot(job_id, url): response = requests.get(url) with open(f'screenshots/{job_id}.png', 'wb') as f: f.write(response.content) print(f'Saved screenshot for job {job_id}') Promise-Based Async Pattern Create a clean interface that submits the job and resolves when the webhook fires: ...