Urlbox

Python website screenshots

Take website screenshots in Python and Django with the Urlbox SDK — signed render links for templates, direct downloads, and async renders with webhooks.

The urlbox Python package wraps the Urlbox API in a single UrlboxClient. Use it to:

  • Generate a render link — a signed URL for an <img> tag in a Django or Jinja template. The browser fetches the screenshot, so your view returns straight away.
  • Download a screenshot — fetch the image bytes in a script, Celery task or management command.
  • Render asynchronously — start a render and get the result by webhook or by polling. Best for long or bulk renders.

See Sync vs Async for the trade-offs.

Install

pip install urlbox

Find your API key and secret in your project's settings on the dashboard. Read them from the environment rather than committing them:

import os

from urlbox import UrlboxClient

urlbox = UrlboxClient(
    api_key=os.environ["URLBOX_API_KEY"],
    api_secret=os.environ["URLBOX_SECRET"],
)
screenshot_url = urlbox.generate_url({
    "url": "https://github.com",
    "thumb_width": 600,
    "format": "jpg",
    "quality": 80,
})

The link is signed with your secret, so nobody can change the options and spend your credits. Pass it to your template:

<img src="{{ screenshot_url }}" alt="Screenshot of github.com">

The first request renders the page, and later requests for the same link are served from the cache. See Render links for caching and refresh options.

Download a screenshot

get() renders the page and returns a requests response with the image bytes:

response = urlbox.get({
    "url": "https://example.com",
    "full_page": True,
    "hide_cookie_banners": True,
    "block_ads": True,
})
response.raise_for_status()

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

The Content-Type header matches the format you asked for (image/png by default, application/pdf for "format": "pdf", and so on).

Render asynchronously

post() starts a render and returns immediately with a renderId and a statusUrl:

response = urlbox.post({
    "url": "https://example.com",
    "full_page": True,
    # Omit webhook_url if you'd rather poll (option 2 below).
    "webhook_url": "https://your-app.com/webhooks/urlbox",
})
render = response.json()
render_id, status_url = render["renderId"], render["statusUrl"]

Then collect the result in one of two ways.

Option 1: receive a webhook

Urlbox posts the result to your webhook_url when the render finishes. Check the X-Urlbox-Signature header with the SDK's webhook_validator before you trust the payload. It needs your project's webhook secret, which is different from your API secret. Here it is in Flask:

import os

from flask import Flask, abort, request
from urlbox import InvalidHeaderSignatureError, webhook_validator

app = Flask(__name__)


@app.post("/webhooks/urlbox")
def urlbox_webhook():
    payload = request.get_json()
    try:
        webhook_validator.call(
            request.headers.get("X-Urlbox-Signature", ""),
            payload,
            os.environ["URLBOX_WEBHOOK_SECRET"],
        )
    except InvalidHeaderSignatureError:
        abort(401)

    if payload["event"] == "render.succeeded":
        print(payload["renderId"], payload["result"]["renderUrl"])
    return "", 200

The validator also rejects signatures more than five minutes old, which stops replayed requests. See Webhooks for the payload format.

Option 2: poll the status URL

No public endpoint to receive webhooks (a script, a notebook, a cron job)? Poll status_url until the render finishes:

import os
import time

import requests


def wait_for_render(status_url, interval=2, timeout=300):
    deadline = time.monotonic() + timeout
    headers = {"Authorization": f"Bearer {os.environ['URLBOX_SECRET']}"}
    while time.monotonic() < deadline:
        render = requests.get(status_url, headers=headers, timeout=30).json()
        if render["status"] == "succeeded":
            return render  # includes renderUrl and size
        if render["status"] in ("failed", "not-found"):
            raise RuntimeError(render.get("reason", render["status"]))
        time.sleep(interval)  # "created" or "retrying": still working
    raise TimeoutError("Timed out waiting for render")


render_url = wait_for_render(status_url)["renderUrl"]

The SDK warns when you call post() without a webhook_url; polling like this is the alternative it means. Send your secret with each poll, so only your project can read the render's status.

Calling the API without the SDK

To build render links yourself, sign the query string with HMAC-SHA256 using your API secret:

import hmac
from hashlib import sha256
from urllib.parse import urlencode


def render_link(options, api_key, api_secret, fmt="png"):
    query = urlencode(options, True)
    token = hmac.new(api_secret.encode(), query.encode(), sha256).hexdigest()
    return f"https://api.urlbox.com/v1/{api_key}/{token}/{fmt}?{query}"

Next steps

On this page