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:
| Parameter | Type | Default | Description |
|---|---|---|---|
url | string | — | The webpage to capture |
format | string | png | Output: png, jpeg, webp, avif, pdf |
width | integer | 1280 | Viewport width in pixels |
height | integer | 720 | Viewport height in pixels |
full_page | boolean | false | Capture the entire scrollable page |
device | string | desktop | Device type: desktop, mobile, tablet |
retina | boolean | false | Render at 2x pixel density |
dark_mode | boolean | false | Force dark color scheme |
selector | string | — | CSS selector to capture a specific element |
delay | integer | 0 | Wait N milliseconds before capturing |
block_cookies | boolean | false | Remove cookie consent banners |
block_ads | boolean | false | Remove advertisements |
block_chats | boolean | false | Remove live chat widgets |
css | string | — | Inject custom CSS before capture |
js | string | — | Execute 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 | |
|---|---|---|
| Setup | Install Chromium, match driver versions, configure fonts | One HTTP GET request |
| Dependencies | selenium + chromedriver or playwright + Chromium binary | requests (already in most projects) |
| Memory per capture | Each browser tab adds a couple hundred megabytes | Zero on your server |
| Scaling | Thread pools, browser reuse strategies, OOM monitoring | Handled by the API |
| Cookie banners | Custom dismiss scripts per site, ongoing maintenance | One parameter: block_cookies=True |
| Font rendering | Install font packages on every server and container | Consistent across all captures |
| Cost | Server CPU/RAM + engineering time | Free 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 KeyFrequently asked questions
requests installed, you can start capturing screenshots immediately. For async workloads, install httpx with pip install httpx. No browser binaries, no system-level dependencies.
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.
delay parameter for pages that load data asynchronously, or wait_for_selector to wait until a specific element appears before capturing.
Retry-After header. The retry function shown in the error handling section handles this automatically.