ScreenshotNeo

BlogHow-to

ScreenshotAPI Webhook Setup for Completed Screenshot Jobs

ScreenshotAPI callbacks currently return HTTP 503 without charging a credit. Here is the documented future webhook flow and the synchronous option to use now.

By the ScreenshotNeo team4 October 20268 min read

ScreenshotAPI’s async webhook callbacks are currently unavailable on the documented deployment. Callback requests return HTTP 503 without charging a credit, and the documentation recommends synchronous rendering. The webhook_url flow below describes the intended protocol for when storage is enabled; it is not a working setup to rely on today.

For current jobs, send a synchronous screenshot request and keep the request open until the image bytes return. If you need a callback-based workflow now, use a provider that documents available async jobs and signed webhooks, such as ScreenshotNeo, whose async jobs support signed webhooks. See its API documentation.

Current availability and what to use

The official ScreenshotAPI reference says callbacks are unavailable on this deployment, return 503, and do not consume a credit. Use synchronous rendering unless the provider confirms that async callbacks have been enabled for your deployment.

Approach Availability in the reviewed docs How completion arrives
Synchronous screenshot Documented current option Image bytes in the HTTP response
Async callback Currently unavailable; 503 without charge Intended flow: initial 202, then a result POST to your webhook

The caller must keep a synchronous request open while ScreenshotAPI renders and returns the response. That is ordinary request-response integration guidance; the docs do not promise a maximum duration.

Use synchronous rendering now

The current screenshot endpoint is POST /v1/screenshot. Authenticate with your API key using X-Api-Key or a Bearer token. On success, the response body contains raw image bytes.

cURL

curl --fail-with-body \
  -X POST "https://screenshotapis.org/v1/screenshot" \
  -H "X-Api-Key: $SCREENSHOTAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com"}' \
  --output screenshot.png

Keep the API key in an environment variable or secret store. The output file should use the image format returned by the service; check the response content type if your integration needs to select an extension dynamically.

Python

import os
import requests

api_key = os.environ["SCREENSHOTAPI_KEY"]
response = requests.post(
    "https://screenshotapis.org/v1/screenshot",
    headers={
        "X-Api-Key": api_key,
        "Content-Type": "application/json",
    },
    json={"url": "https://example.com"},
    timeout=120,
)
response.raise_for_status()

content_type = response.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
    raise RuntimeError(f"Expected image response, got {content_type!r}")

with open("screenshot.png", "wb") as image_file:
    image_file.write(response.content)

Node.js

const apiKey = process.env.SCREENSHOTAPI_KEY;
if (!apiKey) throw new Error("Set SCREENSHOTAPI_KEY first");

const response = await fetch("https://screenshotapis.org/v1/screenshot", {
  method: "POST",
  headers: {
    "X-Api-Key": apiKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ url: "https://example.com" }),
  signal: AbortSignal.timeout(120_000),
});

if (!response.ok) {
  throw new Error(`ScreenshotAPI returned HTTP ${response.status}: ${await response.text()}`);
}
const contentType = response.headers.get("content-type") || "";
if (!contentType.startsWith("image/")) {
  throw new Error(`Expected image response, got ${contentType}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
await import("node:fs/promises").then(({ writeFile }) => writeFile("screenshot.png", bytes));

Operational notes

  • Choose a client timeout long enough for rendering; a client-side timeout does not establish whether the remote render completed.
  • Check the HTTP status before treating the body as an image. Error responses may not contain image bytes.
  • Do not log API keys or put them in browser-side code. A server-side request keeps credentials out of page source.
  • If a caller disconnects or times out, retry deliberately: the reviewed docs do not specify idempotency behavior for repeated synchronous requests.

Documented intended webhook protocol

This section describes the protocol shown in ScreenshotAPI’s documentation for when storage is enabled. The documentation’s examples should not be interpreted as proof that callback delivery currently works on this deployment.

  1. Expose an HTTPS endpoint on your server that can accept a POST containing a JSON result.
  2. Submit the screenshot request with url and webhook_url in the JSON body, authenticated with X-Api-Key.
  3. The documented immediate acknowledgement is HTTP 202 with a render_id, status: "processing", and callback URL.
  4. When processing finishes, the intended flow POSTs a result payload to your callback URL. Validate the signature against the exact raw body before trusting or processing the payload.
curl -X POST "https://screenshotapis.org/v1/screenshot" \
  -H "X-Api-Key: $SCREENSHOTAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "webhook_url": "https://your-domain.example/hooks/screenshotapi"
  }'

The documented completion payload includes these fields:

{
  "render_id": "render identifier",
  "success": true,
  "url": "result URL",
  "content_type": "image/png",
  "render_time_ms": 1234,
  "output_size_bytes": 45678,
  "error": null,
  "timestamp": 1730000000
}

These are example field names and values from the published protocol. Treat the payload as untrusted until signature verification succeeds; check success and handle a failed result even after a valid signature.

Verify the webhook signature

The docs describe an X-Webhook-Signature header containing the hexadecimal HMAC-SHA256 digest of the JSON request body, signed with the API key. Verification must use the exact raw request bytes: parsing and reserializing JSON can change whitespace or key order and produce a different digest. Compare digests in constant time.

Python receiver example

import hashlib
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

API_KEY = os.environ["SCREENSHOTAPI_KEY"].encode("utf-8")

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path != "/hooks/screenshotapi":
            self.send_error(404)
            return

        length = int(self.headers.get("Content-Length", "0"))
        raw_body = self.rfile.read(length)
        supplied = self.headers.get("X-Webhook-Signature", "")
        expected = hmac.new(API_KEY, raw_body, hashlib.sha256).hexdigest()
        if not hmac.compare_digest(supplied, expected):
            self.send_error(401, "Invalid webhook signature")
            return

        try:
            event = json.loads(raw_body)
        except (UnicodeDecodeError, json.JSONDecodeError):
            self.send_error(400, "Invalid JSON")
            return

        # Persist or enqueue the event before acknowledging receipt.
        print("Verified render event:", event.get("render_id"), event.get("success"))
        self.send_response(204)
        self.end_headers()

HTTPServer(("127.0.0.1", 8080), Handler).serve_forever()

This small standard-library example is suitable for illustrating verification, not as a complete production server. Put the route behind HTTPS, enforce a request size limit, avoid logging sensitive payload data, and hand work to a durable queue if processing could take time. The reference does not specify retry cadence, delivery guarantees, or an event identifier beyond the render identifier, so do not assume exactly-once delivery.

Node.js signature check

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(rawBody, suppliedHex, apiKey) {
  const expected = createHmac("sha256", apiKey)
    .update(rawBody)
    .digest();
  let supplied;
  try {
    supplied = Buffer.from(suppliedHex, "hex");
  } catch {
    return false;
  }
  return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}

// In an HTTP framework, capture the raw request body before JSON parsing.
// Then call verifyWebhook(rawBody, req.headers["x-webhook-signature"], apiKey).

Make sure the framework gives this code the original request bytes, not a parsed object. Reject missing, malformed, or wrong-length signatures before accepting an event.

Production checklist for callbacks

  • Use an HTTPS callback URL reachable by the provider.
  • Verify the HMAC over raw bytes with the API key and a constant-time comparison.
  • Validate required fields and the expected render identifier after signature verification.
  • Record or enqueue the event durably, then return a success response promptly.
  • Make handling idempotent by tracking processed render_id values, in case a sender retries.
  • Set payload size limits and avoid exposing secrets or result URLs in logs.
  • Confirm callback availability and retry behavior with the provider before depending on the flow.

Troubleshooting

Symptom Likely cause What to do
Webhook request returns HTTP 503 Callbacks are currently unavailable on the documented deployment. Use synchronous rendering; the docs say this 503 does not charge a credit. Ask the provider to confirm when callbacks are enabled.
No callback arrives after a 202 example The 202 is part of the intended protocol example, not current availability confirmation. Do not rely on the example as a live service guarantee; switch to the synchronous endpoint for now.
Signature check fails The body was parsed and serialized again, the wrong secret was used, or the signature header was altered. Capture raw bytes, use the API key as the HMAC secret, compute SHA-256 hex, and compare in constant time.
Image file contains an error response The client saved a non-success HTTP body as if it were image data. Check the status and content type before writing bytes as an image; inspect error text securely.
Synchronous client times out Rendering took longer than the client timeout or the connection was interrupted. Use an appropriate timeout and handle retry carefully. The docs do not specify idempotency or whether a timed-out render continued.
Callback endpoint rejects a legitimate event Proxy or middleware changed the body, or the handler read the wrong path/header. Preserve the raw body, verify the exact route and X-Webhook-Signature header, and avoid body transformations before verification.

Performance, reliability, and cost

Synchronous rendering is the documented current option, but it ties up a client connection until the image response arrives. Set a bounded timeout and run calls in a background worker for long-running application workflows. If a request times out, the caller cannot infer from that alone whether remote processing finished.

For the intended callback design, acknowledge quickly after signature verification and durable enqueueing, then process the result asynchronously within your own system. Deduplicate by render_id to make retries safe. The reviewed reference does not document callback retries, delivery guarantees, retention duration, or the exact result URL lifetime, so confirm those details before designing around them.

ScreenshotAPI’s documentation says the unavailable callback request returns 503 without charging a credit. It does not provide enough information here to state broader pricing, synchronous failure billing, or retry costs.

Or skip the browser setup

ScreenshotNeo offers screenshot capture through one API call, with async jobs and signed webhooks when you need completion notifications. Its API also has a synchronous option:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation. Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, no card required.

FAQ

Can I enable ScreenshotAPI callbacks with a request parameter?

The documented parameter is webhook_url, but the docs say callbacks are currently unavailable on this deployment. Adding the parameter does not make callback delivery available.

Does a callback HTTP 503 consume a credit?

The reviewed documentation says no: callback requests return 503 without charging a credit.

What should I build if my service requires asynchronous completion?

Use a currently available async screenshot service with documented signed webhooks, or run synchronous rendering from your own background worker and store the returned image. ScreenshotNeo supports async jobs with signed webhooks.