Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prerequisites and installation

Install Python dependencies

Create a virtual environment, then install FastAPI, an ASGI server, and Playwright:

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.