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",
"timestamp": "2025-01-15T10:30:05Z",
"data": {
"jobId": "abc123xyz0",
"url": "https://example.com",
"status": "COMPLETED",
"resultUrl": "https://api.allscreenshots.com/v1/screenshots/jobs/abc123xyz0/result",
"storageUrl": "https://storage.allscreenshots.com/abc123xyz0.png",
"format": "PNG",
"width": 1920,
"height": 1080,
"fileSize": 245678,
"renderTimeMs": 1234,
"completedAt": "2025-01-15T10:30:05Z"
}
}Successful capture with multi-output
When the job used the outputs array, the webhook includes per-output details:
{
"event": "JOB_COMPLETED",
"timestamp": "2025-01-15T10:30:05Z",
"data": {
"jobId": "abc123xyz0",
"url": "https://example.com",
"status": "COMPLETED",
"resultUrl": "https://api.allscreenshots.com/v1/screenshots/jobs/abc123xyz0/result/screenshot",
"storageUrl": "https://storage.allscreenshots.com/abc123xyz0/screenshot.png",
"format": "MULTI",
"width": 1920,
"height": 1080,
"fileSize": 246878,
"renderTimeMs": 1234,
"completedAt": "2025-01-15T10:30:05Z",
"outputs": {
"screenshot": {
"type": "screenshot",
"contentType": "image/png",
"size": 245678,
"resultUrl": "https://api.allscreenshots.com/v1/screenshots/jobs/abc123xyz0/result/screenshot",
"storageUrl": "https://storage.allscreenshots.com/abc123xyz0/screenshot.png"
},
"markdown": {
"type": "markdown",
"contentType": "text/markdown",
"size": 1200,
"resultUrl": "https://api.allscreenshots.com/v1/screenshots/jobs/abc123xyz0/result/markdown",
"storageUrl": "https://storage.allscreenshots.com/abc123xyz0/content.md"
}
}
}
}Failed capture
{
"event": "JOB_FAILED",
"timestamp": "2025-01-15T10:31:00Z",
"data": {
"jobId": "abc123xyz0",
"url": "https://example.com",
"status": "FAILED",
"errorCode": "TIMEOUT",
"errorMessage": "Page load timed out after 60 seconds",
"failedAt": "2025-01-15T10:31:00Z"
}
}Bulk job completed
{
"event": "BULK_COMPLETED",
"timestamp": "2025-01-15T10:30:45Z",
"data": {
"bulkJobId": "bulkabc123",
"totalJobs": 10,
"completedJobs": 9,
"failedJobs": 1,
"completedAt": "2025-01-15T10:30:45Z",
"jobs": [
{
"jobId": "job001abc",
"url": "https://example1.com",
"status": "COMPLETED",
"resultUrl": "https://api.allscreenshots.com/v1/screenshots/jobs/job001abc/result"
},
{
"jobId": "job002abc",
"url": "https://example2.com",
"status": "FAILED",
"errorCode": "TIMEOUT",
"errorMessage": "Page load timed out"
}
]
}
}Scheduled capture
{
"event": "SCHEDULE_EXECUTED",
"timestamp": "2025-01-15T14:00:05Z",
"data": {
"scheduleId": "schedabc123",
"scheduleName": "Daily homepage",
"executionId": "exec001abc",
"url": "https://example.com",
"status": "COMPLETED",
"resultUrl": "https://api.allscreenshots.com/v1/screenshots/jobs/job001abc/result",
"storageUrl": "https://storage.allscreenshots.com/job001abc.png",
"fileSize": 245678,
"renderTimeMs": 1234,
"executedAt": "2025-01-15T14:00:05Z",
"nextExecutionAt": "2025-01-16T14:00:00Z"
}
}Verifying signatures
To ensure webhooks are from AllScreenshots, verify the signature using your webhook secret.
Signature header
When you provide a webhookSecret, each webhook includes a signature in the X-Webhook-Signature header:
X-Webhook-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-webhook-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-Webhook-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 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 | 8 seconds |
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_EXECUTED | 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 data.jobId, data.bulkJobId, or data.executionId to deduplicate, depending on the event:
app.post('/webhooks/screenshot', async (req, res) => {
const { jobId } = req.body.data;
// 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"
}