DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

FastAPI Screenshot API: Capture Web Pages from Your Python AI Backend

FastAPI Screenshot API: Capture Web Pages from Your Python AI Backend

FastAPI is the framework of choice for modern Python backends — especially for AI agents and ML services that need to capture web pages as evidence, screenshots, or structured data.

But here's the problem: if your AI agent backend needs to capture a website screenshot, you can't use Selenium or Puppeteer. Those libraries block the event loop. Your async FastAPI handlers would hang, and you'd lose the entire benefit of async/await.

This is exactly the problem PageBolt solves. Instead of managing local browsers, you just make one httpx call from your async function. Your screenshot is ready in under a second — no blocking, no browser dependencies, no event loop delays.

The Problem: Why Selenium/Puppeteer Breaks FastAPI

FastAPI is built on async. Your handlers are async def, your database calls are async, your HTTP requests are async. Everything flows without blocking.

But Selenium and Puppeteer are blocking libraries. They can't coexist with FastAPI's event loop:

  • Blocks the event loop: Selenium spins up a browser process synchronously. While it's waiting for screenshots, your entire FastAPI app is frozen — no other requests can be processed
  • Not designed for async: Puppeteer (even with asyncpg wrappers) doesn't integrate cleanly with Python's asyncio. You end up running it in a thread pool, which defeats the purpose of async
  • Browser management burden: You need to keep a browser instance alive, monitor it for crashes, handle timeouts, manage memory
  • Breaks serverless: If you're deploying to AWS Lambda, Google Cloud Run, or Modal, you can't run browsers. Period.

PageBolt removes all of this. It's a REST API. One httpx.post() call. Your FastAPI async handler never blocks.

The Solution: REST API That Works With Async

Here's the whole idea in one example:

from fastapi import FastAPI
import httpx
import os

app = FastAPI()

PAGEBOLT_API_KEY = os.getenv("PAGEBOLT_API_KEY")
PAGEBOLT_BASE_URL = "https://api.pagebolt.dev"

@app.post("/capture-screenshot")
async def capture_screenshot(url: str):
    """
    Async endpoint that captures a screenshot
    One HTTP call — never blocks the event loop
    """
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{PAGEBOLT_BASE_URL}/screenshot",
            headers={"Authorization": f"Bearer {PAGEBOLT_API_KEY}"},
            json={
                "url": url,
                "format": "png",
                "width": 1280,
                "height": 720,
                "fullPage": True
            }
        )

    return {
        "status": "success",
        "size": len(response.content),
        "image_base64": response.content.hex()
    }
Enter fullscreen mode Exit fullscreen mode

Done. No blocking. No browsers. Your async handler processes the request without freezing the event loop.

Complete FastAPI Example 1: Async Screenshot Endpoint

Let's build a FastAPI service with a screenshot endpoint that integrates cleanly with async:

from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
import httpx
import os
import tempfile
from pydantic import BaseModel

app = FastAPI()

PAGEBOLT_API_KEY = os.getenv("PAGEBOLT_API_KEY")
PAGEBOLT_BASE_URL = "https://api.pagebolt.dev"

class CaptureRequest(BaseModel):
    url: str
    format: str = "png"
    full_page: bool = True

@app.post("/api/screenshot")
async def capture_screenshot(request: CaptureRequest):
    """
    Async endpoint to capture a screenshot
    Returns: PNG file as response
    """
    if not request.url:
        raise HTTPException(status_code=400, detail="url is required")

    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{PAGEBOLT_BASE_URL}/screenshot",
                headers={"Authorization": f"Bearer {PAGEBOLT_API_KEY}"},
                json={
                    "url": request.url,
                    "format": request.format,
                    "width": 1280,
                    "height": 720,
                    "fullPage": request.full_page
                }
            )

        if response.status_code != 200:
            raise HTTPException(
                status_code=500,
                detail=f"PageBolt error: {response.status_code}"
            )

        # Save to temp file and return
        with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
            tmp.write(response.content)
            tmp_path = tmp.name

        return FileResponse(
            tmp_path,
            media_type="image/png",
            filename=f"screenshot.png"
        )

    except httpx.TimeoutException:
        raise HTTPException(status_code=504, detail="Screenshot capture timed out")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/api/pdf")
async def capture_pdf(request: CaptureRequest):
    """
    Async endpoint to capture a PDF
    """
    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{PAGEBOLT_BASE_URL}/pdf",
                headers={"Authorization": f"Bearer {PAGEBOLT_API_KEY}"},
                json={
                    "url": request.url,
                    "format": "A4",
                    "margin": "1cm"
                }
            )

        if response.status_code != 200:
            raise HTTPException(status_code=500, detail="PDF generation failed")

        with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
            tmp.write(response.content)
            tmp_path = tmp.name

        return FileResponse(
            tmp_path,
            media_type="application/pdf",
            filename=f"document.pdf"
        )

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
Enter fullscreen mode Exit fullscreen mode

Usage:

curl -X POST "http://localhost:8000/api/screenshot" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://pagebolt.dev"}' \
  -o screenshot.png
Enter fullscreen mode Exit fullscreen mode

Complete FastAPI Example 2: BackgroundTasks for Async Processing

For high-traffic APIs, you'll want async task processing. FastAPI's BackgroundTasks integrates perfectly with PageBolt:

from fastapi import FastAPI, BackgroundTasks, HTTPException
from pydantic import BaseModel
import httpx
import os
import uuid
from typing import Dict
from datetime import datetime

app = FastAPI()

PAGEBOLT_API_KEY = os.getenv("PAGEBOLT_API_KEY")
PAGEBOLT_BASE_URL = "https://api.pagebolt.dev"

# In-memory job storage (use Redis/database in production)
jobs: Dict[str, dict] = {}

class ScreenshotJob(BaseModel):
    url: str
    format: str = "png"

async def capture_screenshot_task(job_id: str, url: str, format: str):
    """
    Background task to capture screenshot
    Runs independently of the HTTP response
    """
    try:
        jobs[job_id]["status"] = "processing"

        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{PAGEBOLT_BASE_URL}/screenshot",
                headers={"Authorization": f"Bearer {PAGEBOLT_API_KEY}"},
                json={
                    "url": url,
                    "format": format,
                    "width": 1280,
                    "height": 720,
                    "fullPage": True
                }
            )

        if response.status_code == 200:
            jobs[job_id]["status"] = "completed"
            jobs[job_id]["data"] = response.content
        else:
            jobs[job_id]["status"] = "failed"
            jobs[job_id]["error"] = f"PageBolt error: {response.status_code}"

    except Exception as e:
        jobs[job_id]["status"] = "failed"
        jobs[job_id]["error"] = str(e)

@app.post("/api/screenshot-async")
async def screenshot_async(job_request: ScreenshotJob, background_tasks: BackgroundTasks):
    """
    Queue a screenshot capture job
    Returns immediately with job_id for polling
    """
    job_id = str(uuid.uuid4())

    jobs[job_id] = {
        "status": "queued",
        "url": job_request.url,
        "created_at": datetime.utcnow().isoformat()
    }

    # Queue the background task
    background_tasks.add_task(
        capture_screenshot_task,
        job_id,
        job_request.url,
        job_request.format
    )

    return {
        "job_id": job_id,
        "status": "queued",
        "poll_url": f"/api/screenshot-status/{job_id}"
    }

@app.get("/api/screenshot-status/{job_id}")
async def screenshot_status(job_id: str):
    """
    Poll the status of a screenshot job
    """
    job = jobs.get(job_id)

    if not job:
        raise HTTPException(status_code=404, detail="Job not found")

    status = job["status"]

    if status == "queued" or status == "processing":
        return {"status": status, "job_id": job_id}
    elif status == "completed":
        return {
            "status": "completed",
            "job_id": job_id,
            "url": job["url"],
            "download_url": f"/api/screenshot-download/{job_id}"
        }
    else:
        return {"status": "failed", "error": job.get("error")}, 500

@app.get("/api/screenshot-download/{job_id}")
async def screenshot_download(job_id: str):
    """
    Download the completed screenshot
    """
    job = jobs.get(job_id)

    if not job or job["status"] != "completed":
        raise HTTPException(status_code=404, detail="Screenshot not ready")

    from fastapi.responses import FileResponse
    import tempfile

    with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
        tmp.write(job["data"])
        tmp_path = tmp.name

    return FileResponse(tmp_path, media_type="image/png")
Enter fullscreen mode Exit fullscreen mode

Usage:

# Queue a screenshot
curl -X POST "http://localhost:8000/api/screenshot-async" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://pagebolt.dev"}'
# Returns: { "job_id": "abc-123", "status": "queued" }

# Poll status
curl "http://localhost:8000/api/screenshot-status/abc-123"

# Download when ready
curl "http://localhost:8000/api/screenshot-download/abc-123" -o screenshot.png
Enter fullscreen mode Exit fullscreen mode

Real-World Example: AI Agent Evidence Capture

Here's a practical example — an AI agent backend that captures screenshots as proof of actions:

from fastapi import FastAPI
from pydantic import BaseModel
import httpx
import os
import json

app = FastAPI()

PAGEBOLT_API_KEY = os.getenv("PAGEBOLT_API_KEY")
PAGEBOLT_BASE_URL = "https://api.pagebolt.dev"

class AgentAction(BaseModel):
    action: str  # "visit", "click", "fill", etc.
    url: str
    description: str

@app.post("/api/agent-action")
async def log_agent_action(action: AgentAction):
    """
    AI agent backend logs an action and captures proof
    """
    # Perform the agent action (visit a page, click something, etc.)
    # ...

    # Capture proof as screenshot
    async with httpx.AsyncClient() as client:
        screenshot_response = await client.post(
            f"{PAGEBOLT_BASE_URL}/screenshot",
            headers={"Authorization": f"Bearer {PAGEBOLT_API_KEY}"},
            json={
                "url": action.url,
                "format": "png",
                "width": 1280,
                "height": 720,
                "fullPage": True
            }
        )

    # Store the action + screenshot as evidence
    evidence = {
        "action": action.action,
        "url": action.url,
        "description": action.description,
        "screenshot_bytes": screenshot_response.content.hex(),
        "timestamp": str(datetime.utcnow())
    }

    # Log to database, audit trail, or external service
    # ...

    return {
        "status": "logged",
        "evidence_id": "evt_123",
        "action": action.action,
        "screenshot_captured": screenshot_response.status_code == 200
    }
Enter fullscreen mode Exit fullscreen mode

Comparison: PageBolt vs Selenium/Puppeteer vs Self-Hosted

Feature PageBolt Selenium Puppeteer
Async support Perfect (REST API) Blocks event loop Requires thread pool
Python integration One httpx.post() Heavy library Node.js only
Works in FastAPI Yes, native No, blocks No, blocks
Serverless friendly Yes (Lambda/Cloud Run) No No
Browser management None (hosted) Manual Manual
CPU/memory cost Hosted (not your problem) Your infrastructure Your infrastructure
Cost at scale $29/mo for 10k requests $100+/mo (self-hosted) $100+/mo (self-hosted)

Winner for FastAPI: PageBolt. Native async, no blocking, works everywhere.

Cost Analysis

PageBolt pricing:

  • Free tier: 100 requests/month
  • Paid: $29/month for 10,000 requests (~$0.003 per screenshot)

Self-hosted Selenium in FastAPI:

  • EC2/Lambda layer: $50–$150/month
  • Browser management: 10+ hours setup, 4+ hours/month maintenance
  • Event loop problems: Hiring engineers to fix blocking issues
  • Real cost: $100–$200/month + your time

At 1,000 requests/month, PageBolt saves you money on infrastructure alone.

Why FastAPI + PageBolt Is Natural for AI Agents

FastAPI is the framework of choice for AI agents because:

  1. Async-first design: Your agent makes API calls, processes data, triggers actions — all without blocking
  2. Performance: FastAPI handles hundreds of concurrent requests with minimal overhead
  3. ML integration: Works with OpenAI API, Claude, LLMs — all async-native
  4. Serverless: Deploy to Modal, AWS Lambda, Google Cloud Run without managing servers

PageBolt fits perfectly:

  • Your agent needs screenshots as evidence? One async call.
  • Need to capture a page your agent just modified? Zero blocking.
  • Scaling to 1,000 concurrent agents? No browser overhead.

Next Steps

  1. Get a free API key: Visit pagebolt.dev and sign up — 100 requests/month, no credit card required
  2. Add to your FastAPI app: Copy the capture_screenshot_task function and wire it into your routes
  3. Use with your AI agent: Let your backend capture screenshots without blocking the event loop
  4. Scale as needed: If you exceed 100 requests/month, upgrade to a paid plan ($29/month, cancel anytime)

FastAPI developers shouldn't manage browsers. With PageBolt, your AI agent backend gets web capture in minutes, not weeks.

Try it free — 100 requests/month, no credit card. Start capturing screenshots now.

Top comments (0)