How to Take Screenshots with Flask Using a Screenshot API
Flask's micro-framework philosophy — minimal core, composable extensions — makes it easy to add a screenshot API endpoint in minutes. Whether you're building a monitoring dashboard, a social preview image generator, or a content archiving tool, this guide covers the full range of Flask integration patterns.
Setup
pip install flask requests
Get a free API key at hermesforge.dev/screenshot. Set it in your environment:
export SCREENSHOT_API_KEY=your_api_key_here
Basic Route
A minimal Flask route that proxies screenshot requests to the API:
# app.py
import os
import requests
from flask import Flask, request, Response, jsonify
app = Flask(__name__)
API_KEY = os.environ['SCREENSHOT_API_KEY']
API_BASE = 'https://hermesforge.dev/api/screenshot'
@app.route('/screenshot')
def screenshot():
url = request.args.get('url')
if not url:
return jsonify(error='url parameter is required'), 400
params = {
'url': url,
'width': request.args.get('width', 1280),
'height': request.args.get('height', 800),
'format': request.args.get('format', 'webp'),
}
resp = requests.get(API_BASE, params=params, headers={'X-API-Key': API_KEY})
if not resp.ok:
return jsonify(error='Screenshot failed', status=resp.status_code), resp.status_code
return Response(
resp.content,
content_type=resp.headers.get('Content-Type', 'image/webp'),
headers={'Cache-Control': 'public, max-age=300'},
)
if __name__ == '__main__':
app.run(debug=True)
Test it:
curl "http://localhost:5000/screenshot?url=https://example.com" --output example.webp
Streaming Response
For large screenshots, stream from the upstream API directly to the client:
import requests
from flask import Flask, request, Response, stream_with_context
@app.route('/screenshot/stream')
def screenshot_stream():
url = request.args.get('url')
if not url:
return {'error': 'url required'}, 400
params = {
'url': url,
'format': request.args.get('format', 'webp'),
'width': request.args.get('width', 1280),
'height': request.args.get('height', 800),
}
upstream = requests.get(
API_BASE,
params=params,
headers={'X-API-Key': API_KEY},
stream=True, # Don't buffer upstream response
)
if not upstream.ok:
return {'error': f'Upstream error: {upstream.status_code}'}, upstream.status_code
return Response(
stream_with_context(upstream.iter_content(chunk_size=8192)),
content_type=upstream.headers.get('Content-Type', 'image/webp'),
)
stream_with_context is important here — it keeps the Flask application context alive while the generator runs, which is needed for logging and request-scoped resources.
Blueprint Organization
For larger applications, organize with a Blueprint:
# blueprints/screenshot.py
import os
import requests
from flask import Blueprint, request, Response, jsonify, current_app
screenshot_bp = Blueprint('screenshot', __name__, url_prefix='/api')
API_BASE = 'https://hermesforge.dev/api/screenshot'
def _get_api_key():
return current_app.config.get('SCREENSHOT_API_KEY') or os.environ['SCREENSHOT_API_KEY']
@screenshot_bp.route('/screenshot')
def capture():
url = request.args.get('url')
if not url:
return jsonify(error='url is required'), 400
try:
from urllib.parse import urlparse
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return jsonify(error='Only http/https URLs allowed'), 400
except Exception:
return jsonify(error='Invalid URL'), 400
params = {
'url': url,
'width': request.args.get('width', 1280, type=int),
'height': request.args.get('height', 800, type=int),
'format': request.args.get('format', 'webp'),
'full_page': request.args.get('full_page', 'false'),
}
if delay := request.args.get('delay', type=int):
params['delay'] = delay
resp = requests.get(API_BASE, params=params, headers={'X-API-Key': _get_api_key()}, timeout=30)
if not resp.ok:
return jsonify(error='Screenshot failed'), resp.status_code
return Response(
resp.content,
content_type=resp.headers.get('Content-Type', 'image/webp'),
headers={'Cache-Control': 'public, max-age=300'},
)
# app.py
from flask import Flask
from blueprints.screenshot import screenshot_bp
app = Flask(__name__)
app.config['SCREENSHOT_API_KEY'] = os.environ['SCREENSHOT_API_KEY']
app.register_blueprint(screenshot_bp)
Caching with Flask-Caching
Avoid redundant API calls for identical requests using Flask-Caching:
pip install Flask-Caching
from flask import Flask, request, Response, jsonify
from flask_caching import Cache
app = Flask(__name__)
app.config['CACHE_TYPE'] = 'SimpleCache' # In-memory; use 'RedisCache' for production
app.config['CACHE_DEFAULT_TIMEOUT'] = 300 # 5 minutes
cache = Cache(app)
def _cache_key():
"""Generate a stable cache key from query parameters."""
url = request.args.get('url', '')
width = request.args.get('width', '1280')
height = request.args.get('height', '800')
fmt = request.args.get('format', 'webp')
return f'screenshot:{url}:{width}:{height}:{fmt}'
@app.route('/screenshot')
@cache.cached(timeout=300, key_prefix=_cache_key)
def screenshot():
url = request.args.get('url')
if not url:
return jsonify(error='url required'), 400
params = {'url': url, 'width': request.args.get('width', 1280),
'height': request.args.get('height', 800), 'format': request.args.get('format', 'webp')}
resp = requests.get(API_BASE, params=params, headers={'X-API-Key': API_KEY})
if not resp.ok:
return jsonify(error='Screenshot failed'), resp.status_code
return Response(resp.content, content_type=resp.headers.get('Content-Type', 'image/webp'))
For production, switch to Redis cache:
app.config['CACHE_TYPE'] = 'RedisCache'
app.config['CACHE_REDIS_URL'] = 'redis://localhost:6379/0'
Input Validation with Flask-WTF / Marshmallow
Validate request parameters before forwarding upstream:
# Using marshmallow for schema validation
from marshmallow import Schema, fields, validate, ValidationError
class ScreenshotSchema(Schema):
url = fields.Url(required=True, schemes={'http', 'https'})
width = fields.Int(load_default=1280, validate=validate.Range(min=1, max=3840))
height = fields.Int(load_default=800, validate=validate.Range(min=1, max=2160))
format = fields.Str(load_default='webp', validate=validate.OneOf(['webp', 'png', 'jpeg']))
full_page = fields.Bool(load_default=False)
delay = fields.Int(load_default=None, validate=validate.Range(min=0, max=10000), allow_none=True)
screenshot_schema = ScreenshotSchema()
@app.route('/screenshot')
def screenshot():
try:
params = screenshot_schema.load(request.args)
except ValidationError as err:
return jsonify(errors=err.messages), 400
api_params = {k: v for k, v in params.items() if v is not None}
resp = requests.get(API_BASE, params=api_params, headers={'X-API-Key': API_KEY})
if not resp.ok:
return jsonify(error='Screenshot failed'), resp.status_code
return Response(resp.content, content_type='image/webp')
Async View (Flask 2.0+)
Flask 2.0 supports async views natively with asyncio:
pip install flask[async] aiohttp
import aiohttp
from flask import Flask, request, Response, jsonify
app = Flask(__name__)
@app.route('/screenshot')
async def screenshot():
url = request.args.get('url')
if not url:
return jsonify(error='url required'), 400
params = {
'url': url,
'width': request.args.get('width', 1280),
'height': request.args.get('height', 800),
'format': 'webp',
}
async with aiohttp.ClientSession() as session:
async with session.get(
API_BASE,
params=params,
headers={'X-API-Key': os.environ['SCREENSHOT_API_KEY']},
) as resp:
if not resp.ok:
return jsonify(error=f'Upstream error {resp.status}'), resp.status
data = await resp.read()
content_type = resp.headers.get('Content-Type', 'image/webp')
return Response(data, content_type=content_type)
Note: Flask async views use a thread pool under the hood (via Werkzeug's async handling), not a full async event loop. For true async concurrency, use Quart (async Flask-compatible framework) or FastAPI.
Batch Endpoint
Accept multiple URLs and return all results:
from flask import Flask, request, jsonify
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
@app.route('/screenshots/batch', methods=['POST'])
def batch_screenshot():
data = request.get_json()
urls = data.get('urls', [])
if not urls or not isinstance(urls, list):
return jsonify(error='urls must be a non-empty array'), 400
if len(urls) > 10:
return jsonify(error='Maximum 10 URLs per batch'), 400
width = data.get('width', 1280)
height = data.get('height', 800)
fmt = data.get('format', 'webp')
def capture_one(url):
params = {'url': url, 'width': width, 'height': height, 'format': fmt}
try:
resp = requests.get(API_BASE, params=params, headers={'X-API-Key': API_KEY}, timeout=30)
if resp.ok:
import base64
return {'url': url, 'status': 'ok',
'data': base64.b64encode(resp.content).decode(),
'contentType': resp.headers.get('Content-Type', 'image/webp')}
return {'url': url, 'status': 'error', 'error': f'HTTP {resp.status_code}'}
except Exception as e:
return {'url': url, 'status': 'error', 'error': str(e)}
results = []
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(capture_one, url): url for url in urls}
for future in as_completed(futures):
results.append(future.result())
return jsonify(results=results)
Error Handler
Register a global error handler for screenshot-related exceptions:
class ScreenshotError(Exception):
def __init__(self, message, status_code=500):
super().__init__(message)
self.status_code = status_code
@app.errorhandler(ScreenshotError)
def handle_screenshot_error(err):
return jsonify(error=str(err)), err.status_code
@app.route('/screenshot')
def screenshot():
url = request.args.get('url')
if not url:
raise ScreenshotError('url is required', 400)
resp = requests.get(API_BASE, params={'url': url, 'format': 'webp'},
headers={'X-API-Key': API_KEY})
if not resp.ok:
raise ScreenshotError(f'Screenshot API returned {resp.status_code}', resp.status_code)
return Response(resp.content, content_type='image/webp')
Production Config
A production-ready Flask app structure with environment-based config:
# config.py
import os
class Config:
SCREENSHOT_API_KEY = os.environ.get('SCREENSHOT_API_KEY', '')
CACHE_TYPE = 'SimpleCache'
CACHE_DEFAULT_TIMEOUT = 300
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max upload
class ProductionConfig(Config):
DEBUG = False
CACHE_TYPE = 'RedisCache'
CACHE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
class DevelopmentConfig(Config):
DEBUG = True
config = {
'production': ProductionConfig,
'development': DevelopmentConfig,
'default': DevelopmentConfig,
}
# app.py
from flask import Flask
from flask_caching import Cache
from config import config
def create_app(env='default'):
app = Flask(__name__)
app.config.from_object(config[env])
cache = Cache(app)
from blueprints.screenshot import screenshot_bp
app.register_blueprint(screenshot_bp)
return app
Deploy with Gunicorn:
pip install gunicorn
gunicorn "app:create_app('production')" --workers 4 --bind 0.0.0.0:8000
Get Your API Key
Free API key at hermesforge.dev/screenshot. Full parameter reference at hermesforge.dev/api/docs.