Webhooks
Receive notifications when screenshots are ready
Webhooks
Receive HTTP notifications when async jobs, bulk jobs, or scheduled captures complete. Webhooks eliminate the need for polling and enable real-time integrations.
Setting up webhooks
Include webhookUrl in your request to enable notifications:
{
"url": "https://example.com",
"webhookUrl": "https://your-server.com/webhooks/screenshot",
"webhookSecret": "your-secret-key"
}Webhook payload
When a job completes, we send a POST request to your webhook URL:
Successful capture
{
"event": "job.completed",
"jobId": "job_abc123xyz",
"status": "completed",
"timestamp": "2025-01-15T10:30:05Z",
"result": {
"url": "https://storage.allscreenshots.com/screenshots/abc123.png",
"expiresAt": "2025-01-30T10:30:05Z",
"size": 245678,
"width": 1920,
"height": 1080,
"format": "png",
"renderTime": 1234
}
}Failed capture
{
"event": "job.failed",
"jobId": "job_abc123xyz",
"status": "failed",
"timestamp": "2025-01-15T10:31:00Z",
"error": {
"code": "timeout",
"message": "Page load timed out after 60 seconds"
}
}Bulk job completed
{
"event": "bulk.completed",
"bulkJobId": "bulk_abc123xyz",
"status": "completed",
"timestamp": "2025-01-15T10:30:45Z",
"summary": {
"total": 10,
"completed": 9,
"failed": 1
},
"jobs": [
{
"jobId": "job_001",
"url": "https://example1.com",
"status": "completed",
"result": { ... }
},
{
"jobId": "job_002",
"url": "https://example2.com",
"status": "failed",
"error": { ... }
}
]
}Scheduled capture
{
"event": "schedule.captured",
"scheduleId": "sched_abc123xyz",
"captureId": "cap_001",
"timestamp": "2025-01-15T14:00:05Z",
"result": {
"url": "https://storage.allscreenshots.com/screenshots/abc123.png",
"size": 245678,
"width": 1920,
"height": 1080
}
}Verifying signatures
To ensure webhooks are from AllScreenshots, verify the signature using your webhook secret.
Signature header
Every webhook includes a signature in the X-Allscreenshots-Signature header:
X-Allscreenshots-Signature: sha256=abc123...Verification code
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express middleware
app.post('/webhooks/screenshot', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-allscreenshots-signature'];
const payload = req.body.toString();
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Process the webhook...
res.sendStatus(200);
});Python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/webhooks/screenshot', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Allscreenshots-Signature')
payload = request.get_data()
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
# Process the webhook...
return '', 200Always verify webhook signatures in production. Without verification, attackers could send fake webhook payloads to your endpoint.
Responding to webhooks
Success response
Return a 2xx status code to acknowledge receipt:
app.post('/webhooks/screenshot', (req, res) => {
// Process webhook asynchronously if needed
processWebhook(req.body).catch(console.error);
// Respond immediately
res.sendStatus(200);
});Retry behavior
If your endpoint returns an error or times out, we retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the webhook is marked as failed.
Webhook endpoints must respond within 30 seconds. For long processing, acknowledge immediately and process asynchronously.
Event types
| Event | Description |
|---|---|
job.completed | Async screenshot job completed successfully |
job.failed | Async screenshot job failed |
bulk.completed | Bulk job completed (all URLs processed) |
schedule.captured | Scheduled capture completed |
schedule.failed | Scheduled capture failed |
Best practices
Use a dedicated endpoint
Create a separate endpoint for AllScreenshots webhooks:
https://your-app.com/webhooks/allscreenshotsProcess asynchronously
Don't block the webhook response with heavy processing:
app.post('/webhooks/screenshot', async (req, res) => {
// Acknowledge immediately
res.sendStatus(200);
// Process in background
try {
await processScreenshot(req.body);
} catch (error) {
console.error('Webhook processing failed:', error);
}
});Store webhook payloads
Log incoming webhooks for debugging:
app.post('/webhooks/screenshot', async (req, res) => {
// Store for debugging
await db.webhookLogs.create({
receivedAt: new Date(),
headers: req.headers,
body: req.body,
});
// Process...
res.sendStatus(200);
});Handle duplicates
Webhooks may be delivered more than once. Use the jobId to deduplicate:
app.post('/webhooks/screenshot', async (req, res) => {
const { jobId } = req.body;
// Check if already processed
const existing = await db.processedJobs.findOne({ jobId });
if (existing) {
return res.sendStatus(200); // Already processed
}
// Mark as processing
await db.processedJobs.create({ jobId, processedAt: new Date() });
// Process webhook...
res.sendStatus(200);
});Testing webhooks
Use tools like webhook.site or ngrok to test locally:
# Start ngrok tunnel
ngrok http 3000
# Use the ngrok URL as your webhook
{
"url": "https://example.com",
"webhookUrl": "https://abc123.ngrok.io/webhooks/screenshot"
}