·SavePage Team

Asynchronous Screenshots with Webhooks

webhooksasyncapi

The standard SavePage.io API is synchronous: you send a request and wait for the screenshot to be captured before receiving a response. This works well for most use cases, but it has limitations for long-running captures and high-volume workflows.

The problem with synchronous requests

A synchronous screenshot request ties up an HTTP connection for the entire capture duration. For a simple page, this is 2-3 seconds. For a full-page capture of a complex SPA with a 5-second delay, it can be 15-20 seconds.

At scale, this creates issues:

  • HTTP timeouts on load balancers (typically 30-60 seconds)
  • Connection pool exhaustion on the client
  • Wasted resources holding connections open while waiting

The webhook approach

With webhooks, the flow changes:

  1. You send a capture request with a callback_url parameter
  2. The API immediately responds with a 202 Accepted and a capture ID
  3. The screenshot is captured asynchronously
  4. When the capture completes, the API sends a POST request to your callback URL with the results
import requests

# Step 1: Initiate capture
response = requests.post(
    "https://api.savepage.io/v1/capture",
    json={
        "url": "https://example.com",
        "width": 1440,
        "height": 900,
        "fullpage": True,
        "callback_url": "https://myapp.example.com/webhooks/screenshot"
    },
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)

data = response.json()
# {"status": "accepted", "capture_id": "cap_abc123"}
print(f"Capture queued: {data['capture_id']}")
# Step 2: Receive the webhook (your server)
from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/screenshot", methods=["POST"])
def handle_screenshot():
    data = request.json
    # {
    #   "capture_id": "cap_abc123",
    #   "status": "success",
    #   "image": "https://cdn.savepage.io/captures/abc123.png",
    #   "width": 1440,
    #   "height": 12400,
    #   "format": "png",
    #   "captured_at": "2025-09-20T12:00:00Z"
    # }

    # Process the result
    save_screenshot(data["capture_id"], data["image"])
    return "OK", 200

Advantages

No connection timeout risk. Your HTTP request completes immediately. The capture can take as long as it needs.

Better resource utilization. Your client is not blocked waiting for screenshots. It can submit many captures and process results as they arrive.

Natural batching. Submit 100 capture requests in a loop, then process the 100 webhook callbacks as they complete. No need to manage concurrent HTTP connections.

Retry handling. If your webhook endpoint is temporarily unavailable, the API retries the callback with exponential backoff.

Webhook security

To verify that webhook payloads come from SavePage.io and not a malicious third party:

  1. Each webhook includes an X-Signature header
  2. The signature is an HMAC-SHA256 of the request body using your webhook secret
  3. Verify the signature before processing the payload
import hmac
import hashlib

def verify_webhook(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

When to use webhooks vs synchronous

Use synchronous requests when:

  • You need the screenshot immediately (interactive tools, live previews)
  • Capture time is predictable and short (viewport-only, no delay)
  • Volume is low (under 100/hour)

Use webhooks when:

  • Captures are long-running (full page, high delay)
  • You are processing batches of URLs
  • Your client cannot hold long connections
  • You want fire-and-forget submission

Both approaches use the same rendering infrastructure. The difference is in how results are delivered.