Features Pricing Docs Blog Playground Log In Sign Up

Webhooks

Webhooks let you receive real-time notifications when screenshots are ready, instead of polling the API. When a screenshot completes or fails, we send a POST request to your specified URL with the event details.

Setup

Include a webhook_url parameter when creating a screenshot:

bash
curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "webhook_url": "https://yourapp.com/webhooks/screenshot"
  }'

Your webhook_url must:

  • Be a publicly accessible HTTPS endpoint
  • Accept POST requests with a JSON body
  • Respond with a 2xx status code within 30 seconds

Webhook Payload

When a screenshot event occurs, we send a POST request to your webhook URL with the following payload:

screenshot.completed

json
{
  "event": "screenshot.completed",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "completed",
    "url": "https://example.com",
    "options": {
      "width": 1280,
      "height": 800,
      "format": "png",
      "quality": 80,
      "full_page": false,
      "device": "desktop",
      "block_ads": false,
      "block_cookies": true,
      "dark_mode": false,
      "delay": 0,
      "timeout": 30,
      "retina": false
    },
    "file_size": 245760,
    "mime_type": "image/png",
    "width": 1280,
    "height": 800,
    "processing_time_ms": 3450,
    "image_url": "https://screenshotrun.com/api/v1/screenshots/550e8400-.../image",
    "created_at": "2026-03-09T10:30:00.000000Z",
    "completed_at": "2026-03-09T10:30:05.000000Z"
  },
  "sent_at": "2026-03-09T10:30:05.500000Z"
}

screenshot.failed

json
{
  "event": "screenshot.failed",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "failed",
    "url": "https://example.com",
    "error": {
      "message": "Page load timed out after 30 seconds.",
      "code": "TIMEOUT"
    },
    "created_at": "2026-03-09T10:30:00.000000Z"
  },
  "sent_at": "2026-03-09T10:30:35.000000Z"
}

Events

EventDescriptionWhen it fires
screenshot.completed Screenshot captured successfully Image is ready for download via the image_url
screenshot.failed Screenshot capture failed An error occurred during capture (timeout, invalid URL, etc.)

Receiving Webhooks

Here are examples of how to handle incoming webhooks in different languages:

PHP (Laravel)

php
// routes/web.php
Route::post('/webhooks/screenshot', [WebhookController::class, 'handle']);

// app/Http/Controllers/WebhookController.php
class WebhookController extends Controller
{
    public function handle(Request $request)
    {
        $event = $request->input('event');
        $data = $request->input('data');

        match ($event) {
            'screenshot.completed' => $this->handleCompleted($data),
            'screenshot.failed' => $this->handleFailed($data),
            default => null,
        };

        return response()->json(['received' => true]);
    }

    private function handleCompleted(array $data): void
    {
        // Download and save the image
        $image = Http::withToken(config('services.screenshotapi.key'))
            ->get($data['image_url']);

        Storage::put("screenshots/{$data['id']}.png", $image->body());
    }

    private function handleFailed(array $data): void
    {
        Log::error("Screenshot failed: {$data['error']['message']}", $data);
    }
}

Python (Flask)

python
from flask import Flask, request, jsonify
import requests
import os

app = Flask(__name__)

@app.route("/webhooks/screenshot", methods=["POST"])
def handle_webhook():
    payload = request.get_json()
    event = payload["event"]
    data = payload["data"]

    if event == "screenshot.completed":
        # Download and save the image
        headers = {"Authorization": f"Bearer {os.environ['SCREENSHOT_API_KEY']}"}
        image = requests.get(data["image_url"], headers=headers)

        with open(f"screenshots/{data['id']}.png", "wb") as f:
            f.write(image.content)

    elif event == "screenshot.failed":
        print(f"Screenshot failed: {data['error']['message']}")

    return jsonify({"received": True})

JavaScript (Express)

javascript
const express = require("express");
const fs = require("fs");
const app = express();

app.use(express.json());

app.post("/webhooks/screenshot", async (req, res) => {
  const { event, data } = req.body;

  if (event === "screenshot.completed") {
    // Download and save the image
    const response = await fetch(data.image_url, {
      headers: { "Authorization": `Bearer ${process.env.SCREENSHOT_API_KEY}` },
    });
    const buffer = Buffer.from(await response.arrayBuffer());
    fs.writeFileSync(`screenshots/${data.id}.png`, buffer);
  }

  if (event === "screenshot.failed") {
    console.error(`Screenshot failed: ${data.error.message}`);
  }

  res.json({ received: true });
});

app.listen(3000);

Ruby (Sinatra)

ruby
require "sinatra"
require "json"
require "net/http"

post "/webhooks/screenshot" do
  payload = JSON.parse(request.body.read)
  event = payload["event"]
  data = payload["data"]

  case event
  when "screenshot.completed"
    # Download and save the image
    uri = URI(data["image_url"])
    req = Net::HTTP::Get.new(uri)
    req["Authorization"] = "Bearer #{ENV['SCREENSHOT_API_KEY']}"
    image = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }

    File.binwrite("screenshots/#{data['id']}.png", image.body)
  when "screenshot.failed"
    puts "Screenshot failed: #{data['error']['message']}"
  end

  { received: true }.to_json
end

Retry Policy

If your endpoint doesn't return a 2xx status code, we retry the webhook delivery with exponential backoff:

AttemptDelayTotal time elapsed
1st retry30 seconds~30 seconds
2nd retry2 minutes~2.5 minutes
3rd retry10 minutes~12.5 minutes

After 3 failed attempts, the webhook is marked as failed and no further retries are made. You can check webhook delivery status in your dashboard.

Security

Webhook payloads are currently not signed. To verify that a webhook is genuine, we recommend the following approach:

  • Verify via API — When you receive a webhook, call GET /api/v1/screenshots/{id} with your API key to confirm the screenshot exists and matches the webhook data
  • Use a secret path — Include a random token in your webhook URL (e.g., https://yourapp.com/webhooks/screenshot?token=your_secret) and reject requests without a valid token
  • Restrict by IP — If possible, whitelist our server IPs in your firewall rules

Best Practices

  • Respond quickly — Return a 200 response immediately, then process the webhook asynchronously (e.g., using a queue)
  • Handle duplicates — Webhook deliveries may arrive more than once in rare cases. Use the screenshot id to deduplicate
  • Use HTTPS — Always use HTTPS endpoints to ensure payload security in transit
  • Log payloads — Log incoming webhooks for debugging and auditing purposes
  • Handle unknown events — Gracefully ignore events you don't recognize, in case we add new event types in the future

Testing Webhooks

During development, you can use tools like ngrok or webhook.site to receive webhooks on your local machine:

bash
# Start an ngrok tunnel to your local server
ngrok http 3000

# Use the ngrok URL as your webhook_url
curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "webhook_url": "https://abc123.ngrok.io/webhooks/screenshot"
  }'