Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Dark Mode Image Format & Quality MCP Server Pricing Docs Blog Log In Sign Up

Python Screenshot API — Capture Website Screenshots with One Request

Python developers who automate screenshots usually start with Selenium or Playwright. Both work on a local machine. Move that same code to a production server or a Docker container, and you end up installing Chromium, matching driver versions, configuring font packages, and watching memory climb every time another browser tab opens. A CI pipeline that used to finish in two minutes now takes eight because of the Chromium download step.

The ScreenshotRun Python screenshot API cuts that down to one HTTP call. Pass a URL to the endpoint, get back a PNG, JPEG, WebP, AVIF, or PDF. The standard requests library is all you need, and most Python projects already have it installed.

Your first screenshot in 30 seconds

Sign up at the dashboard to get an API key (the free plan gives you 200 captures per month, no credit card). Then run this:

import requests

response = requests.get(
    "https://api.screenshotrun.com/v1/screenshots/capture",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={"url": "https://github.com", "response_type": "image"},
    timeout=60,
)

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

print("Saved: github.png")

No Chromium download, no driver version to match, no virtual display to configure. The API spins up a real Chrome instance on our infrastructure, waits for the page to fully render (JavaScript, lazy-loaded images, web fonts), and sends back the binary image.

Full example with requests

A production-ready wrapper with configurable output format and error handling:

import requests
import os

API_KEY = os.environ["SCREENSHOTRUN_KEY"]

def capture_screenshot(target_url: str, **options) -> bytes:
    params = {
        "url": target_url,
        "format": options.get("format", "png"),
        "width": options.get("width", 1280),
        "height": options.get("height", 720),
        "full_page": options.get("full_page", False),
        "response_type": "image",
    }

    resp = requests.get(
        "https://api.screenshotrun.com/v1/screenshots/capture",
        headers={"Authorization": f"Bearer {API_KEY}"},
        params=params,
        timeout=60,
    )
    resp.raise_for_status()
    return resp.content


screenshot = capture_screenshot(
    "https://github.com",
    format="webp",
    width=1920,
    full_page=True,
)

with open("github-full.webp", "wb") as f:
    f.write(screenshot)

print("Captured full-page WebP screenshot")

The API key lives in an environment variable (SCREENSHOTRUN_KEY). This keeps secrets out of your repository and makes key rotation a one-line config change.

Async with httpx

If your application already runs on an async stack (FastAPI, aiohttp, or any asyncio-based framework), blocking calls from requests will stall the event loop. httpx provides an async HTTP client that drops into the same pattern:

import httpx
import os

API_KEY = os.environ["SCREENSHOTRUN_KEY"]

async def capture_screenshot_async(target_url: str, **options) -> bytes:
    params = {
        "url": target_url,
        "format": options.get("format", "png"),
        "width": options.get("width", 1280),
        "height": options.get("height", 720),
        "full_page": options.get("full_page", False),
        "response_type": "image",
    }

    async with httpx.AsyncClient(timeout=60) as client:
        resp = await client.get(
            "https://api.screenshotrun.com/v1/screenshots/capture",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=params,
        )
        resp.raise_for_status()
        return resp.content

The function signature stays the same. Swap requests.get for httpx.AsyncClient.get, add async/await, and the rest of your code looks identical. Install with pip install httpx.

Available parameters

Every parameter is passed as a query string argument in the GET request. This table covers the most common options:

ParameterTypeDefaultDescription
urlstringThe webpage to capture
formatstringpngOutput: png, jpeg, webp, avif, pdf
widthinteger1280Viewport width in pixels
heightinteger720Viewport height in pixels
full_pagebooleanfalseCapture the entire scrollable page
devicestringdesktopDevice type: desktop, mobile, tablet
retinabooleanfalseRender at 2x pixel density
dark_modebooleanfalseForce dark color scheme
selectorstringCSS selector to capture a specific element
delayinteger0Wait N milliseconds before capturing
block_cookiesbooleanfalseRemove cookie consent banners
block_adsbooleanfalseRemove advertisements
block_chatsbooleanfalseRemove live chat widgets
cssstringInject custom CSS before capture
jsstringExecute JavaScript before capture

The full parameter reference is in the API documentation. These options cover most production screenshot tasks.

Dark mode, element capture, and CSS injection

These three capabilities appear in most production setups.

Dark mode. Add dark_mode=True and the API sets prefers-color-scheme: dark before the page loads. Sites that respect this media query render their dark theme automatically:

params = {
    "url": "https://github.com",
    "dark_mode": True,
    "response_type": "image",
}

Element capture. Isolate a single page element using its CSS selector. The API trims the output to that element's bounding box:

params = {
    "url": "https://github.com/anthropics",
    "selector": ".js-repo-list",
    "response_type": "image",
}

CSS injection. Hide a promotional banner or change fonts before the capture fires. Pass raw CSS through the css parameter:

params = {
    "url": "https://example.com",
    "css": ".promo-banner { display: none !important; } body { font-family: Inter, sans-serif; }",
    "response_type": "image",
}

All three combine in a single request. One call can capture a specific element in dark mode with injected styles.

Handling errors and retries

Network requests fail. Target sites go down, rate limits apply, servers occasionally hiccup. A production integration needs retry logic:

import requests
import time
import os

API_KEY = os.environ["SCREENSHOTRUN_KEY"]

def capture_with_retry(url: str, params: dict = None, max_retries: int = 2) -> bytes:
    request_params = {
        "url": url,
        "format": "png",
        "response_type": "image",
        **(params or {}),
    }

    for attempt in range(max_retries + 1):
        resp = requests.get(
            "https://api.screenshotrun.com/v1/screenshots/capture",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=request_params,
            timeout=60,
        )

        if resp.ok:
            return resp.content

        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 5))
            time.sleep(wait)
            continue

        if resp.status_code >= 500 and attempt < max_retries:
            time.sleep(1 * (attempt + 1))
            continue

        resp.raise_for_status()

    raise RuntimeError("Max retries exceeded")

The function backs off on rate-limit responses (429) and retries on server errors (5xx) with increasing delays. Client errors like invalid parameters or bad credentials raise immediately because retrying those accomplishes nothing.

FastAPI proxy endpoint

A common pattern: your frontend needs screenshots, but the API key must stay server-side. FastAPI makes this straightforward:

from fastapi import FastAPI, Query
from fastapi.responses import Response
import httpx
import os

app = FastAPI()

@app.get("/api/screenshot")
async def screenshot_proxy(
    url: str = Query(..., description="URL to capture"),
    format: str = Query("png", description="Output format"),
    width: int = Query(1280, description="Viewport width"),
):
    async with httpx.AsyncClient(timeout=60) as client:
        resp = await client.get(
            "https://api.screenshotrun.com/v1/screenshots/capture",
            headers={"Authorization": f"Bearer {os.environ['SCREENSHOTRUN_KEY']}"},
            params={
                "url": url,
                "format": format,
                "width": width,
                "response_type": "image",
            },
        )

    if not resp.is_success:
        return Response(content=resp.text, status_code=resp.status_code)

    return Response(
        content=resp.content,
        media_type=resp.headers.get("content-type", "image/png"),
        headers={"Cache-Control": "public, max-age=3600"},
    )

Your client calls /api/screenshot?url=https://example.com and gets the image back. The API key never leaves the server. The Cache-Control header prevents duplicate API calls for the same URL within the cache window. The same pattern works with Flask or Django by swapping the framework-specific parts.

API vs. self-hosted Selenium and Playwright

Selenium and Playwright are good tools for browser automation. If your workflow involves filling login forms, navigating multi-step flows, or running end-to-end tests, they remain the right choice. When the job is converting URLs into images at scale, the infrastructure overhead adds up fast.

Self-hosted (Selenium/Playwright)ScreenshotRun API
SetupInstall Chromium, match driver versions, configure fontsOne HTTP GET request
Dependenciesselenium + chromedriver or playwright + Chromium binaryrequests (already in most projects)
Memory per captureEach browser tab adds a couple hundred megabytesZero on your server
ScalingThread pools, browser reuse strategies, OOM monitoringHandled by the API
Cookie bannersCustom dismiss scripts per site, ongoing maintenanceOne parameter: block_cookies=True
Font renderingInstall font packages on every server and containerConsistent across all captures
CostServer CPU/RAM + engineering timeFree tier (200/month), then usage-based

If your app needs a real browser for interactive flows like logging in or clicking through forms, Selenium or Playwright is the way to go. If the goal is turning URLs into images without running Chrome on your own servers, an API call means less code and fewer things breaking in production.

Sign up, copy your API key, and paste it into one of the examples above. The free tier covers 200 captures per month.

Get Your Free API Key

Frequently asked questions

No. The API is a standard HTTP endpoint. If your project already has requests installed, you can start capturing screenshots immediately. For async workloads, install httpx with pip install httpx. No browser binaries, no system-level dependencies.
Yes. Use httpx.AsyncClient instead of requests to make non-blocking calls. The page includes a complete FastAPI proxy endpoint example that you can drop into your project. The same approach works with any asyncio-based framework.
A typical capture takes 2-5 seconds depending on the target page. That is comparable to a well-tuned local setup. The difference shows at scale: the API handles concurrency and browser management for you, while Selenium or Playwright need thread pools, browser reuse strategies, and enough RAM for parallel instances.
Yes. The API runs a full Chromium browser, so JavaScript-heavy pages (React, Vue, Angular, single-page applications) render completely. Use the delay parameter for pages that load data asynchronously, or wait_for_selector to wait until a specific element appears before capturing.
The free plan includes 200 screenshots per month with a per-second rate limit to prevent abuse. If you hit a rate limit, the API returns a 429 status with a Retry-After header. The retry function shown in the error handling section handles this automatically.