Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
FastAPI can expose a screenshot endpoint by combining an async Playwright browser with a validated URL parameter. Install both the Python package and Playwright’s browser binaries, navigate to the destination, call page.screenshot(), and return the resulting bytes with an image media type. The examples below cover viewport, full-page, element, PNG/JPEG/WebP, quality, device scale, and an alternative hosted API.
What you will build
The endpoint in this tutorial accepts a URL and returns an image. It renders pages in Chromium through Playwright, so JavaScript-driven sites can finish loading before capture. You can return a visible viewport, the entire scrollable document, or one element selected with CSS.
This is a practical starting point, not a complete production deployment design. The available framework material does not establish a universal browser-pooling, concurrency, timeout, or deployment recipe. Treat the security and operations sections below as safeguards you must adapt and verify for your environment.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsPrerequisites and installation
Install Python dependencies
Create a virtual environment, then install FastAPI, an ASGI server, and Playwright:
#1 Best Overall
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install fastapi uvicorn playwright
Install browser binaries
Installing the Python package alone is not enough. Download the supported browser binaries:
playwright install chromium
On a Linux host, Playwright also offers a dependency-install command, but whether you can run it depends on your image and privileges. Build those system libraries into your container or base image rather than assuming a production machine has them.
Minimal asynchronous FastAPI screenshot endpoint
Save this as main.py. The application launches one browser when the process starts, creates a fresh context and page for each request, and closes those per-request resources in a finally block. The screenshot is held in memory and returned directly.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
from contextlib import asynccontextmanager
from io import BytesIO
from typing import Literal
from urllib.parse import urlparse
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import Response
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError
@asynccontextmanager
async def lifespan(app: FastAPI):
playwright = await async_playwright().start()
browser = await playwright.chromium.launch(headless=True)
app.state.playwright = playwright
app.state.browser = browser
yield
await browser.close()
await playwright.stop()
app = FastAPI(lifespan=lifespan)
def validate_url(value: str) -> str:
parsed = urlparse(value)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise HTTPException(status_code=400, detail="url must be an absolute http or https URL")
return value
@app.get("/screenshot")
async def screenshot(
url: str = Query(..., description="Absolute http(s) URL"),
full_page: bool = False,
format: Literal["png", "jpeg", "webp"] = "png",
quality: int | None = Query(default=None, ge=0, le=100),
width: int = Query(default=1280, ge=320, le=3840),
height: int = Query(default=720, ge=200, le=2160),
selector: str | None = None,
):
target = validate_url(url)
if format == "png" and quality is not None:
raise HTTPException(status_code=400, detail="quality does not apply to PNG")
browser = app.state.browser
context = await browser.new_context(viewport={"width": width, "height": height})
page = await context.new_page()
try:
await page.goto(target, wait_until="networkidle", timeout=30_000)
if selector:
image = await page.locator(selector).screenshot(
type=format,
quality=quality,
)
else:
image = await page.screenshot(
type=format,
quality=quality,
full_page=full_page,
)
except PlaywrightTimeoutError:
raise HTTPException(status_code=504, detail="page navigation or capture timed out")
except Exception as exc:
raise HTTPException(status_code=502, detail=f"capture failed: {exc}")
finally:
await page.close()
await context.close()
media_type = {"png": "image/png", "jpeg": "image/jpeg", "webp": "image/webp"}[format]
return Response(content=image, media_type=media_type)
Run it with:
uvicorn main:app --reload
Then request an image:
curl "http://127.0.0.1:8000/screenshot?url=https%3A%2F%2Fexample.com" -o example.png
The endpoint returns an HTTP 200 image on success, a 400 error for an invalid URL or incompatible options, a 504 when navigation exceeds the timeout, and a 502 for another browser failure.
Screenshot options that matter
Viewport versus full page
By default, Playwright captures the current viewport. Set full_page=True to capture the complete scrollable page. Full-page rendering can be substantially taller and heavier than a viewport shot; impose dimensions or byte-size limits appropriate to your service.
Capture one element
When selector is supplied, the example calls page.locator(selector).screenshot(). This is useful for a chart, invoice, card, or component. A selector that matches nothing causes a capture error, so return a clear client error after checking the locator or add an explicit wait for dynamically inserted content.
Rank #2
PNG, JPEG, and WebP
PNG is lossless and supports transparent pixels. JPEG is usually smaller for photographs; WebP can provide a compact modern format. The quality option applies to JPEG and WebP, not PNG. The response media type must match the selected format.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →CSS pixels and device scale
The width and height values define the viewport in CSS pixels. Playwright’s screenshot API also supports a device scale factor, which distinguishes CSS dimensions from physical output pixels. Add a device_scale_factor to browser.new_context() when you need a retina-style image:
context = await browser.new_context(
viewport={"width": 1280, "height": 720},
device_scale_factor=2,
)
Waiting for the right state
wait_until="networkidle" is convenient for many pages but can be a poor fit for applications that keep long-lived connections open. Alternatives include waiting for a specific selector or applying a short, deliberate delay:
await page.goto(target, wait_until="domcontentloaded", timeout=30_000)
await page.locator("main.dashboard").wait_for(state="visible", timeout=10_000)
# Or, only when necessary:
await page.wait_for_timeout(1_000)
Use the narrowest condition that represents “ready” for your page; arbitrary sleeps make every request slower and can still miss late content.
Returning bytes, storing files, or returning a URL
Calling page.screenshot() without a path returns bytes, which is appropriate for a FastAPI Response, object storage upload, hashing, or post-processing. Supplying path="screenshot.png" writes a file instead:
await page.screenshot(path="screenshot.png", full_page=True)
If clients should receive a durable URL rather than image bytes, upload those bytes to storage you control and return a JSON object containing that URL. The storage lifecycle, access control, and CDN behavior are application decisions; the screenshot API itself does not define them.
Rank #3
Security and request validation
A public “fetch any URL” endpoint is a server-side request proxy. Validate the scheme, impose maximum URL length, and consider an allowlist of domains. In a production network, block access to loopback, private, link-local, metadata, and internal DNS destinations; URL parsing alone does not prevent DNS rebinding or redirects to internal hosts. Restrict redirects or validate every hop, cap response size, and run the browser with a least-privilege account and an isolated network where possible.
Do not forward arbitrary incoming headers or cookies to destinations. If authenticated pages are required, define an explicit credential mechanism, keep secrets out of query strings and logs, and never expose them in error messages. Add authentication and rate limits to your FastAPI route before making it reachable from the internet.
Resource lifecycle, concurrency, and reliability
The sample demonstrates a clear lifecycle: browser startup during application lifespan, a new context per request, and cleanup in finally. It does not claim that one browser per worker is optimal for your workload. Measure memory and latency with your page mix before choosing worker counts, a browser pool, or a queue.
Recommended Free Tools
- Set navigation and selector timeouts so a broken origin cannot hold a request forever.
- Limit concurrent captures to protect CPU and memory; reject or queue excess work.
- Log URL, duration, status, output format, and failure category, but redact credentials and sensitive query parameters.
- Use retries cautiously. Retrying a page with side effects can be unsafe, and repeated browser launches increase load.
- Close contexts even when navigation or screenshot fails.
The cited FastAPI example that screenshots its own /docs page is illustrative, not a production deployment pattern. Validate your ASGI worker model, container dependencies, and observability separately.
Common failures and fixes
“Executable doesn’t exist”
Install the browser binary with playwright install chromium in the same environment that runs Uvicorn. In containers, run that command during the image build.
Timeout while loading
Check DNS, outbound firewall rules, TLS, and the destination’s response time. Increase the timeout only when justified; prefer waiting for a meaningful selector instead of an unlimited navigation.
Rank #4
Blank or incomplete screenshot
The page may render content after the chosen load event, require scrolling to trigger lazy images, or show a consent dialog. Wait for a stable selector, scroll deliberately when needed, and inspect the page in headed mode during debugging.
Free tools Windows power users keep installed
One-click scans. No signup required.
Element not found
Confirm the selector in browser developer tools and wait for it to become visible. If the element is inside an iframe, locate the appropriate frame before selecting it.
Huge memory use
Full-page captures and high device scale factors multiply pixel count. Cap viewport dimensions, avoid unnecessary retina scale, limit concurrent contexts, and return compressed formats when quality permits.
Navigation reaches an unsafe host
Treat this as an SSRF defense failure, not a Playwright bug. Enforce destination policy before navigation and re-check redirects and resolved addresses.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo provides a hosted screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or a PDF, while its capture flow can accept cookie and consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers.
Read the parameter reference in the ScreenshotNeo documentation. A minimal cURL request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Every plan includes the features; the Free plan includes 1,000 shots per month with no card, Starter is $5 for 3,000, and yearly billing provides two months free. Create a free ScreenshotNeo account to start.
Best Value
FastAPI versus a hosted capture service
| Decision | Playwright in your FastAPI app | ScreenshotNeo |
|---|---|---|
| Browser operations | You install and operate Playwright and browser binaries. | Capture is provided through an HTTPS API or MCP server. |
| Output | Return bytes or store them yourself. | PNG, JPEG, WebP, or PDF from one API call. |
| Page controls | Implement waits, selectors, headers, cookies, and policies in your app. | Options include full page, element selectors, waits, custom CSS/JavaScript, blocking, headers, cookies, user agent, timezone, geolocation, caching, signed links, async jobs, bulk capture, and more. |
| Cost evidence | No comparable price or throughput is established here. | Free 1,000/month; paid plans start at $5 for 3,000 shots. |
Choose the in-process route when you need complete control over browser code and network policy and are prepared to operate it. Choose the hosted route when installing browsers and handling capture edge cases is not worth adding to your FastAPI service.
FAQ
Can I use synchronous Playwright with FastAPI?
Yes, Playwright documents synchronous and asynchronous Python APIs. In an async FastAPI route, the asynchronous API avoids blocking the event loop.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallDoes full-page mode capture lazy-loaded images?
It captures the scrollable page, but a site’s lazy-loading implementation may require scrolling or another readiness condition first. Wait for the content your page promises to display.
Can the endpoint return a PDF instead of an image?
Playwright’s page screenshot API produces image formats. PDF generation is a separate browser capability; implement and validate it as a separate response path, or use a service such as ScreenshotNeo that documents PDF capture.
Is this code safe to expose publicly?
Not without authentication, rate limiting, destination controls, redirect checks, and resource limits. A URL-fetching endpoint must be designed as an SSRF-sensitive service.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

