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:

  1. Go to Dashboard → Webhooks
  2. Enter your webhook endpoint URL
  3. Copy your webhook signing secret
  4. 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

  1. Always verify signatures - Prevent spoofed webhook requests
  2. Respond quickly - Return 200 immediately, process asynchronously
  3. Handle duplicates - Webhooks may be delivered more than once
  4. Deduplicate on the payload - Use the body's event and id together to detect a repeat delivery. X-Webhook-ID changes on every retry, so log it for debugging only

See Also

Was this page helpful?