Webhooks
Receive real-time notifications when screenshots complete or fail. Webhooks are sent for both individual screenshots and batch requests.
Supported Events
| Event | Trigger |
|---|---|
screenshot.completed |
Individual screenshot captured successfully |
screenshot.failed |
Individual screenshot capture failed |
batch.completed |
All screenshots in a batch finished |
batch.failed |
Batch processing failed |
Configuration
Configure your webhook URL in the Dashboard Settings:
- Go to Dashboard → Webhooks
- Enter your webhook endpoint URL
- Copy your webhook signing secret
- Use the Send Test button to verify your endpoint
The dashboard webhook receives an event for every screenshot request and for every batch that runs asynchronously. It does not make any request asynchronous.
Each workspace has its own webhook URL and its own signing secret. Events of a request go to the webhook of the API key's workspace and are signed with that workspace's secret. The Webhooks page shows the workspace selected in the sidebar switcher.
Per-request webhook
Add a webhook object to a POST /v1/screenshot body to run that request asynchronously:
{ "url": "https://example.com", "webhook": { "url": "https://your-app.example.com/webhooks/screenshot", "events": ["completed", "failed"], "secret": "your-own-signing-secret" } }
| Field | Default | Notes |
|---|---|---|
webhook.url |
required | Public http or https URL whose host resolves to public addresses at request time, else invalid_parameter |
webhook.events |
["completed"] |
completed, failed |
webhook.secret |
workspace secret | Signs this request's events when set |
The API answers 202 at once:
{ "id": "job_8f3c2a...", "status": "processing", "webhook": "https://your-app.example.com/webhooks/screenshot" }
The job id is the event's id. A credit is charged only when the render succeeds. For batches, webhook.url on POST /v1/batch makes the batch asynchronous; see the async webhooks guide.
Webhook Payload
screenshot.completed
Sent when an individual screenshot is captured successfully:
{ "event": "screenshot.completed", "id": "req_abc123def456", "timestamp": "2024-01-18T12:00:00Z", "data": { "url": "https://example.com", "screenshot_url": "https://cdn.renderscreenshot.com/screenshots/ab/cd/abcd1234.png", "width": 1200, "height": 630, "size": 123456, "format": "png", "cached": false } }
screenshot.failed
Sent when a screenshot capture fails:
{ "event": "screenshot.failed", "id": "req_abc123def456", "timestamp": "2024-01-18T12:00:00Z", "data": { "url": "https://example.com", "error": "Navigation timeout of 30000 ms exceeded", "error_code": "timeout" } }
batch.completed
Sent when all screenshots in a batch have finished processing:
{ "event": "batch.completed", "id": "batch_abc123", "timestamp": "2024-01-18T12:00:00Z", "data": { "summary": { "total": 10, "completed": 9, "failed": 1 }, "results_url": "https://api.renderscreenshot.com/v1/batch/batch_abc123" } }
batch.failed
Sent when batch processing fails entirely:
{ "event": "batch.failed", "id": "batch_abc123", "timestamp": "2024-01-18T12:00:00Z", "data": { "summary": { "total": 10, "completed": 0, "failed": 10 }, "results_url": "https://api.renderscreenshot.com/v1/batch/batch_abc123" } }
Webhook Headers
Every webhook request includes these headers:
POST /webhooks/screenshots HTTP/1.1 Host: your-server.com Content-Type: application/json X-Webhook-ID: whk_abc123def456 X-Webhook-Timestamp: 1705579200 X-Webhook-Signature: sha256=a1b2c3d4e5f6... User-Agent: RenderScreenshot-Webhook/1.0
| Header | Description |
|---|---|
X-Webhook-ID |
Identifier for this delivery attempt. A retry gets a new value, so do not use it to detect duplicates |
X-Webhook-Timestamp |
Unix timestamp when the webhook was sent |
X-Webhook-Signature |
HMAC-SHA256 signature for verification |
Verifying Signatures
Always verify webhook signatures to ensure authenticity. Use the signing secret of the workspace from your dashboard.
Java
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.renderscreenshot.sdk.Webhook; // In your servlet or controller String signature = request.getHeader("X-Webhook-Signature"); String timestamp = request.getHeader("X-Webhook-Timestamp"); String payload = /* read request body as string */; if (Webhook.verify(payload, signature, timestamp, System.getenv("WEBHOOK_SECRET"))) { // The payload names the event in "event". WebhookEvent.getType() reads a "type" field, // so read "event" from the JSON yourself (Jackson shown here). JsonNode event = new ObjectMapper().readTree(payload); String type = event.path("event").asText(); if ("screenshot.completed".equals(type)) { System.out.println("Screenshot ready: " + event.path("data").path("screenshot_url").asText()); } else if ("screenshot.failed".equals(type)) { System.out.println("Screenshot failed: " + event.path("data").path("error").asText()); } response.setStatus(200); } else { response.setStatus(401); }
Node.js
import express from 'express'; import { verifyWebhook, parseWebhook } from 'renderscreenshot'; // In your route handler (Express example). express.raw keeps the exact bytes that were signed; // re-serializing a parsed body with JSON.stringify can change them and fail verification. app.post('/webhooks/screenshot', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; const timestamp = req.headers['x-webhook-timestamp']; const payload = req.body.toString('utf8'); if (verifyWebhook(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) { const event = parseWebhook(payload); if (event.type === 'screenshot.completed') { console.log('Screenshot ready:', event.data.response?.url); } else if (event.type === 'screenshot.failed') { console.error('Screenshot failed:', event.data.error?.message); } res.status(200).send('OK'); } else { res.status(401).send('Invalid signature'); } });
PHP
function verifyWebhook($payload, $signature, $timestamp, $secret) { $expected = hash_hmac('sha256', "$timestamp.$payload", $secret); return hash_equals("sha256=$expected", $signature); } // In your endpoint $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE']; $timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP']; $payload = file_get_contents('php://input'); if (verifyWebhook($payload, $signature, $timestamp, $_ENV['WEBHOOK_SECRET'])) { // Process webhook } else { http_response_code(401); }
Python
import os from flask import Flask, request, jsonify from renderscreenshot import verify_webhook, parse_webhook app = Flask(__name__) @app.route('/webhooks/screenshot', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') timestamp = request.headers.get('X-Webhook-Timestamp') payload = request.get_data(as_text=True) if verify_webhook(payload, signature, timestamp, os.environ['WEBHOOK_SECRET']): event = parse_webhook(request.json) if event['type'] == 'screenshot.completed': print('Screenshot ready:', event['data'].get('response', {}).get('url')) elif event['type'] == 'screenshot.failed': print('Screenshot failed:', event['data'].get('error', {}).get('message')) return jsonify({'status': 'ok'}), 200 else: return jsonify({'error': 'Invalid signature'}), 401
Ruby
def verify_webhook(payload, signature, timestamp, secret) expected = OpenSSL::HMAC.hexdigest( 'SHA256', secret, "#{timestamp}.#{payload}" ) Rack::Utils.secure_compare("sha256=#{expected}", signature) end # In your controller signature = request.headers['X-Webhook-Signature'] timestamp = request.headers['X-Webhook-Timestamp'] payload = request.raw_post if verify_webhook(payload, signature, timestamp, ENV['WEBHOOK_SECRET']) # Process webhook else head :unauthorized end
Retry Policy
A failed delivery is retried with a growing delay (polynomial backoff):
| Attempt | Delay after the previous attempt |
|---|---|
| 1 | Immediate |
| 2 | About 3 seconds |
| 3 | About 20 seconds |
| 4 | About 1.5 minutes |
| 5 | About 4.5 minutes |
All 5 attempts happen within about 7 minutes. After the fifth failed attempt, the webhook is abandoned. If your endpoint can be down for longer, do not rely on the webhook alone. For a batch, read the result with the batch status endpoint.
Responding to Webhooks
Return a 2xx status code to acknowledge receipt:
HTTP/1.1 200 OK
Any non-2xx response triggers a retry.
Testing Webhooks
Dashboard Test Button
Use the Send Test button in your webhook settings to send a test payload to your endpoint.
Local Development
Use tools like webhook.site or ngrok to test webhooks locally:
# Using ngrok ngrok http 3000 # Then configure your webhook URL as: # https://abc123.ngrok.io/webhooks/screenshots
Best Practices
- Always verify signatures - Prevent spoofed webhook requests
- Respond quickly - Return 200 immediately, process asynchronously
- Handle duplicates - Webhooks may be delivered more than once
- Deduplicate on the payload - Use the body's
eventandidtogether to detect a repeat delivery.X-Webhook-IDchanges on every retry, so log it for debugging only
See Also
- Batch Screenshots - Batch processing with webhook notifications
- Error Codes - Understanding error responses
- SDKs - Webhook verification helpers in all official SDKs
- Dashboard Webhooks - Configure your webhook settings
- Webhook verification guide - Verify signatures in each language