How to Use a Screenshot API with Python Flask
Build a Flask endpoint that validates a target URL, calls a hosted screenshot API, and returns image bytes with the correct content type.
A Flask app can expose website screenshots without running Chromium in the Flask process: accept a request, validate the target URL and allowed capture options, call a hosted screenshot API from the server with a bounded timeout, then return the provider’s image bytes with its content type. The provider renders the page; Flask handles your application’s access policy and HTTP response.
How the request flows
The browser or another client calls your Flask route. Flask checks whether the caller may use it, validates the requested destination and capture settings, and uses a server-side credential to call the rendering provider. The provider returns image bytes and metadata, and Flask sends those bytes to its caller.
- Your client requests
/screenshot?url=https://example.com. - Flask validates the caller, URL, and a bounded set of options.
- The Flask server calls the screenshot provider with its API key and timeout.
- Flask returns the image with the provider’s MIME type, or a controlled error.
Keep the provider key on the server. Do not place it in JavaScript, a mobile app, or a response sent to the caller. A route that accepts arbitrary URLs is also a security boundary: an attacker may try to make your server request internal services or cloud metadata endpoints. Define and enforce your own destination policy.
Install Flask and the ScreenshotAPI SDK
This example uses ScreenshotAPI’s Python SDK, whose package is screenshotapi-to and import module is screenshotapi. The SDK documentation lists Python 3.8+ support and a default request timeout of 60 seconds. Set the key in the environment instead of committing it to source control. See the provider’s Flask integration guide and Python SDK documentation for its current contract.
python -m venv .venv
source .venv/bin/activate
python -m pip install Flask screenshotapi-to
On Windows PowerShell, activate with .venv\Scripts\Activate.ps1. Set the credential in the environment before starting the application:
export SCREENSHOTAPI_KEY="your-provider-key"
python app.py
Use your deployment platform’s secret manager or environment configuration in production. Avoid printing the key or including it in error responses.
Build a Flask endpoint that returns the screenshot
The following minimal route checks for a URL, calls the provider with a bounded timeout, and returns the SDK result’s bytes using its supplied content type. The URL policy is deliberately represented by a placeholder; replace it with the application-specific checks described below before exposing this route.
import os
from flask import Flask, Response, jsonify, request
from screenshotapi import ScreenshotAPI
app = Flask(__name__)
client = ScreenshotAPI(os.environ["SCREENSHOTAPI_KEY"], timeout=30.0)
@app.get("/screenshot")
def screenshot():
target = request.args.get("url")
if not target:
return jsonify(error="url is required"), 400
# Replace this placeholder with your destination policy before production.
result = client.screenshot({"url": target, "type": "webp"})
return Response(result.image, mimetype=result.content_type)
if __name__ == "__main__":
app.run()
This is a minimal integration shape, not a complete public-service security policy. Add destination validation, caller authentication, rate limits, option limits, and controlled error handling before deployment. The provider SDK’s result exposes image bytes and content_type; using that content type avoids labeling WebP bytes as PNG.
Validate and constrain the destination
Do not pass an unchecked query parameter directly to a server-side renderer. At minimum, parse the URL and require an allowed scheme, normally HTTPS (and HTTP only if your use case requires it). Decide whether to allow arbitrary public hosts or maintain a host allowlist. Reject loopback, private, link-local, and reserved IP destinations, including IPv4-mapped IPv6 forms; account for DNS resolution and redirects so a public-looking name cannot lead to an internal address. Provider-side rendering does not remove the need to set your own policy.
For a fixed-purpose application, an allowlist of expected hosts is easier to reason about than trying to enumerate every unsafe destination. If you permit arbitrary public sites, use network-level egress controls as an additional boundary where possible. Do not assume that checking the original URL alone is enough if redirects can change the destination.
Expose only the options your application needs
Letting every caller choose arbitrary provider parameters makes workload, cost, and output size harder to control. Define a small input schema. For example, accept only url and a format from png, jpeg, or webp; set viewport dimensions and wait behavior on the server. If you later expose full-page capture, selectors, or wait settings, validate and cap each one. Reject unknown parameters rather than silently forwarding them to the provider.
Return an attachment instead of inline image bytes
If callers should download the file, use Flask’s file response support with an in-memory byte stream. Keep the provider’s MIME type and choose an extension that matches the format you requested.
from io import BytesIO
from flask import send_file
# After obtaining result from client.screenshot(...):
return send_file(
BytesIO(result.image),
mimetype=result.content_type,
as_attachment=True,
download_name="page.webp",
)
For a large response or a provider that returns a hosted URL instead of bytes, follow that provider’s documented response contract. Screenshot APIs differ: do not mix one service’s endpoint, authentication headers, parameter names, or JSON response shape with another service’s SDK.
Direct HTTP call from Flask
You can call a provider’s HTTP API without its SDK, but the endpoint, authentication, parameters, timeout behavior, and response format must all come from that provider’s current documentation. For ScreenshotAPI, the SDK documentation shows a GET request to https://screenshotapi.to/api/v1/screenshot, URL parameters, and the API key in the x-api-key header. The essential pattern is:
import os
import requests
response = requests.get(
"https://screenshotapi.to/api/v1/screenshot",
params={"url": "https://example.com", "type": "webp"},
headers={"x-api-key": os.environ["SCREENSHOTAPI_KEY"]},
timeout=(5, 30), # connect timeout, read timeout
)
response.raise_for_status()
image_bytes = response.content
content_type = response.headers.get("Content-Type", "application/octet-stream")
Install Requests with python -m pip install requests if you use this route. In Flask, catch connection errors, timeouts, and HTTP errors from the provider and map them to a controlled response. Confirm whether the provider returns image bytes directly or a different response before treating the body as an image.
Handle failures without leaking provider details
Map expected failures to useful HTTP status codes and a short message. Log an internal request identifier and a sanitized failure category; do not log credentials, full sensitive URLs, page contents, or raw provider responses unless your data policy allows it.
| Condition | Suggested response | Handling |
|---|---|---|
| Missing or malformed URL | 400 Bad Request | Explain the required input without calling the provider. |
| Caller is not authorized for your route | 401 or 403 | Authenticate callers separately from the provider API key. |
| Destination violates policy | 400 or 403 | Reject before making an upstream request. |
| Provider request times out | 504 Gateway Timeout | Return a short retryable error; avoid unbounded retries. |
| Provider rejects credentials or quota is exhausted | 502 or 503 | Log the provider status safely; do not expose keys or internal response data. |
| Provider returns an unexpected body | 502 Bad Gateway | Check the documented response contract and content type before returning bytes. |
A production route should handle the SDK’s documented exceptions. If you use direct Requests calls, catch requests.Timeout, requests.ConnectionError, and requests.HTTPError. The exact exception classes for SDK calls depend on that SDK’s current documentation; do not assume Requests exceptions apply to its client.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Missing-key error at startup | SCREENSHOTAPI_KEY is unset or unavailable to the process. |
Set it in the shell, container, or secret manager and restart the process. |
| 401 or 403 from the provider | Wrong, revoked, or incorrectly scoped key. | Check the provider account and secret configuration; never put the key in the URL. |
Import error for screenshotapi |
The distribution and import names differ, or the package was installed into another Python environment. | Install screenshotapi-to using the same interpreter that runs Flask. |
| Flask returns HTML or corrupt image | An upstream error or JSON payload was treated as image bytes, or the MIME type was hard-coded incorrectly. | Inspect status and documented response shape; return the provider content type for image bytes. |
| Requests time out | The provider’s render takes longer than the configured timeout or the target page is slow. | Use an explicit bounded timeout suited to your latency budget; simplify capture options or return a controlled timeout response. |
| Unexpected internal content can be captured | The public route accepts arbitrary destinations or does not account for redirects and DNS results. | Enforce a host or IP policy, block private ranges, and add network egress restrictions. |
Performance, reliability, and cost
Each request includes a remote render and an upstream network call, so Flask’s response time depends on the provider and target page. Keep the timeout bounded, limit concurrent work and request rate, and avoid retrying a slow or failed capture several times in the same web request. If callers need many captures, consider a background job pattern supported by your provider rather than holding a web worker open.
Do not promise a latency or availability number unless your provider publishes a current guarantee that applies to your account and region. Check current pricing, monthly quota, retention, and data handling before production. Add your own rate limits so one caller cannot consume the application’s provider quota. Treat URLs and page content sent to a hosted renderer as data shared with a third party under that provider’s terms.
For repeat requests, caching can reduce duplicate upstream calls if your application’s freshness and privacy requirements allow it. Key the cache on a normalized URL plus every capture option that affects the output. Set a finite expiration and avoid sharing cached results across users when pages contain private or personalized information.
Hosted API or Playwright in your own service?
A hosted screenshot API keeps browser operations outside the Flask deployment: the app makes an HTTP request and handles the provider’s contract. Direct Playwright capture gives your application control of a browser process and supports screenshots to files or byte buffers, including full-page and element captures. That control also means operating the browser and its deployment environment yourself. See the Playwright Python screenshot documentation.
| Choose a hosted API when… | Choose direct browser automation when… |
|---|---|
| You want Flask to call a remote renderer rather than package and operate a browser. | You need to own browser lifecycle and implement a browser-driven workflow in your application. |
| The provider supports the target page and capture controls you need. | Your workflow depends on interactions or authenticated state that you must manage in the browser. |
| You prefer provider pricing and quotas over operating browser infrastructure. | You can operate the browser environment and want to manage its infrastructure and cost directly. |
| Your data policy permits sending the target URL and page request to a provider. | You need to keep rendering within infrastructure you control, subject to your own deployment and data controls. |
Check the chosen provider’s current support, contract, pricing, and data terms. The right architecture depends on authentication needs, interaction, deployment constraints, privacy, latency budget, and total operating cost.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Flask can call its endpoint directly; rendering happens through the API, so your Flask process does not need to run a browser. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
For a Flask route, make the same server-side request, keep access_key in an environment secret, check the response status, and return the response bytes with its Content-Type. Apply the same URL validation, caller authorization, rate limits, timeout, and error mapping described above.
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
- The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Does Flask need to install Chromium for a hosted screenshot API?
No. The hosted provider renders remotely; Flask makes an HTTP request and returns the result.
Can I expose this endpoint publicly?
Only with caller access controls, rate limits, a destination policy, and bounded capture options. Otherwise it can become an open proxy or quota-abuse path.
Should I return a URL or image bytes?
Follow the provider contract. The ScreenshotAPI SDK example returns image bytes and a content type; other APIs may return a URL or another response shape.
Can I use an async Flask view?
You can, but an ordinary synchronous SDK call still blocks while it waits for the provider. Use an async-compatible HTTP client or a background job design if your application’s concurrency model requires nonblocking upstream work.


