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()
}
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))
Usage:
curl -X POST "http://localhost:8000/api/screenshot" \
-H "Content-Type: application/json" \
-d '{"url": "https://pagebolt.dev"}' \
-o screenshot.png
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")
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
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
}
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:
- Async-first design: Your agent makes API calls, processes data, triggers actions — all without blocking
- Performance: FastAPI handles hundreds of concurrent requests with minimal overhead
- ML integration: Works with OpenAI API, Claude, LLMs — all async-native
- 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
- Get a free API key: Visit pagebolt.dev and sign up — 100 requests/month, no credit card required
-
Add to your FastAPI app: Copy the
capture_screenshot_taskfunction and wire it into your routes - Use with your AI agent: Let your backend capture screenshots without blocking the event loop
- 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)