ScreenshotNeo

BlogHow-to

How to Use a Screenshot API in a Django Application

Capture website screenshots from Django with a hosted API. Learn secure URL validation, a complete Django view, response handling, and production tradeoffs.

By the ScreenshotNeo team4 October 202610 min read

A Django application can capture a website through a hosted screenshot API by making a server-side HTTP request, validating the submitted URL and options, then returning the provider’s image or PDF bytes—or a permitted result URL. Keep the API key on the server, set timeouts, handle non-success responses, and protect the capture endpoint from abuse.

This is different from Django’s Selenium screenshot support: Selenium screenshots are test artifacts for checking your own application UI, while a hosted screenshot API can power a runtime feature such as a user-requested preview. Django views return an HttpResponse; there is no special screenshot view class required.

1. Decide what the endpoint should return

Before implementing the view, choose the response contract your application needs:

Result Django response Use it when
Image or PDF bytes HttpResponse with the verified media type The caller should display or download the capture through your application.
Provider result URL JsonResponse containing the URL The provider returns a URL and your access rules allow the caller to use it.
Redirect to provider bytes HttpResponseRedirect only if suitable The provider documents a redirect flow and exposing that destination is acceptable.

Do not assume every API mode returns raw bytes. Some modes return JSON by default, and a URL-based flow may expose a publicly accessible capture. Follow the selected provider’s documented response mode and avoid making private captures public unintentionally.

2. Configure the provider and credentials

Use a server-side HTTP client from a Django view or a background worker. Store the key in deployment-managed environment or secret settings; never put it in browser JavaScript, a template, or a committed settings file.

The example below uses the provider contract documented at https://api.screenshot-api.org/api/v1/screenshot: a POST JSON request with bearer-style authorization and an image response mode. It is a vendor-specific illustration; verify endpoint, request fields, and response mode against the provider you select. That provider also documents X-API-Key authentication and recommends header authentication over a query-string secret.

Install the HTTP client if your project does not already use it:

python -m pip install requests

Set a secret outside source control, for example in your deployment environment:

SCREENSHOT_API_KEY=replace-with-your-secret-key

For local development, load secrets from a local environment file that is excluded from version control, or use your normal secret-management tool. Avoid logging authorization headers or secret-bearing URLs.

3. Add URL and option validation

A remote renderer fetches the submitted target URL. Treat it as untrusted input. Prefer an allowlist of hosts your application needs. At minimum, accept only HTTP or HTTPS, reject local and private destinations as appropriate, and account for redirects and DNS resolution when enforcing a security boundary. Validate each capture option and impose sensible bounds, such as maximum viewport dimensions and an allowed output-format list.

Django’s ALLOWED_HOSTS protects validation of incoming Host headers used by Django; it does not validate the outbound target URL sent to a screenshot provider. CSRF protection for a browser-submitted request to your Django endpoint also does not replace target URL validation.

The following helper demonstrates a narrow allowlist policy. Replace the example host set with the hosts your product is meant to capture. For a strong SSRF boundary, enforce destination-address and redirect policy at a layer that can validate resolved addresses on every hop; a hostname check alone is not a complete network security control.

from urllib.parse import urlsplit

ALLOWED_CAPTURE_HOSTS = {"example.com", "www.example.com"}

def is_allowed_capture_url(value):
    if not isinstance(value, str) or len(value) > 2048:
        return False
    try:
        parts = urlsplit(value)
        return (
            parts.scheme in {"http", "https"}
            and bool(parts.hostname)
            and parts.username is None
            and parts.password is None
            and parts.hostname.lower() in ALLOWED_CAPTURE_HOSTS
            and parts.port in {None, 80, 443}
        )
    except ValueError:
        return False

4. Implement a Django view that returns screenshot bytes

This example accepts JSON, validates the URL, requests a PNG capture, applies explicit connect and read timeouts, checks the upstream status and content type, and returns the bytes. It intentionally rejects unexpected response types rather than forwarding arbitrary content.

import json
import os

import requests
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import require_POST

SCREENSHOT_ENDPOINT = "https://api.screenshot-api.org/api/v1/screenshot"
ALLOWED_IMAGE_TYPES = {"image/png", "image/jpeg", "image/webp"}

@require_POST
def capture(request):
    try:
        data = json.loads(request.body)
    except (json.JSONDecodeError, UnicodeDecodeError):
        return JsonResponse({"error": "Request body must be valid JSON."}, status=400)

    target_url = data.get("url") if isinstance(data, dict) else None
    if not is_allowed_capture_url(target_url):
        return JsonResponse({"error": "URL is not allowed."}, status=400)

    image_format = data.get("format", "png")
    if image_format not in {"png", "jpeg", "webp"}:
        return JsonResponse({"error": "Unsupported image format."}, status=400)

    api_key = os.environ.get("SCREENSHOT_API_KEY")
    if not api_key:
        # Log configuration detail internally; do not reveal secrets to callers.
        return JsonResponse({"error": "Screenshot service is not configured."}, status=500)

    try:
        upstream = requests.post(
            SCREENSHOT_ENDPOINT,
            headers={"Authorization": f"Bearer {api_key}"},
            json={"url": target_url, "format": image_format, "fullPage": False},
            timeout=(3.05, 30),
        )
    except requests.RequestException:
        return JsonResponse({"error": "Screenshot provider unavailable."}, status=502)

    if not upstream.ok:
        # Record a sanitized status or provider request ID in server logs if useful.
        return JsonResponse({"error": "Screenshot request failed."}, status=502)

    content_type = upstream.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
    if content_type not in ALLOWED_IMAGE_TYPES:
        return JsonResponse({"error": "Unexpected screenshot response type."}, status=502)

    response = HttpResponse(upstream.content, content_type=content_type)
    response["Content-Disposition"] = 'inline; filename="screenshot"'
    return response

Wire the view into a URL configuration:

from django.urls import path
from .views import capture

urlpatterns = [
    path("api/capture/", capture, name="capture"),
]

Send a request from a trusted client:

curl -X POST http://localhost:8000/api/capture/ \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com","format":"png"}' \
  --output screenshot.png

For a PDF response, permit pdf, send the provider’s documented PDF options, and allow application/pdf through the media-type check. Use an appropriate filename and disposition for your application. Do not accept an arbitrary content type from the caller.

5. Handle providers that return JSON or a URL

If the selected provider returns JSON containing a capture URL, request that documented mode and parse the response as JSON only after checking the upstream status and JSON content type. Validate the URL’s scheme and host against the provider’s documented domains before returning it. Whether you return it as JSON or redirect depends on your authorization and privacy requirements.

# Sketch for a provider whose documented mode returns JSON with a capture URL:
if not upstream.ok:
    return JsonResponse({"error": "Screenshot request failed."}, status=502)

if "application/json" not in upstream.headers.get("Content-Type", ""):
    return JsonResponse({"error": "Unexpected provider response."}, status=502)

try:
    result = upstream.json()
    capture_url = result["url"]
except (ValueError, KeyError, TypeError):
    return JsonResponse({"error": "Malformed provider response."}, status=502)

# Apply provider-host validation and your access policy before exposing capture_url.
return JsonResponse({"url": capture_url})

The field name and shape above are illustrative. Use the actual schema in the provider documentation; do not assume that url is the field name or that a returned URL is private.

6. Add capture options carefully

Most screenshot APIs expose a common set of controls. Names and availability vary by provider, so translate your application’s small set of supported options into the provider’s documented request fields.

Option What it changes Validation to add
Format PNG, JPEG, WebP, or PDF output Allow only formats your endpoint supports; set the expected media type accordingly.
Full page Captures beyond the initial viewport and may load lazy content Limit page height or output size where the provider supports it.
Viewport width and height Controls responsive layout Use integer bounds rather than accepting unlimited dimensions.
Device scale Controls pixel density Bound the scale to manage output size and work.
Element selector Captures a selected page element Limit selector length and document selector behavior for missing elements.
Wait condition or delay Allows content to render before capture Cap fixed waits and prefer a relevant ready condition when available.
Dark mode Emulates a dark color scheme Accept a boolean or small enumerated value.
PDF settings Paper size, margins, landscape, and page ranges Validate each field and keep PDF-only options out of image requests.
Custom CSS or JavaScript Changes the rendered page before capture Expose only to trusted users; arbitrary scripts increase abuse and security risk.
Cookies, headers, or authorization Captures pages requiring a session or custom request context Do not accept credentials for arbitrary targets without a clear trust boundary; never log them.

The reviewed example provider documents GET query parameters and POST JSON. More complex settings, including CSS, JavaScript, hidden selectors, geolocation, timezone, locale, and PDF configuration, are POST-only in its API. Confirm the current schema before relying on any vendor-specific parameter.

7. Protect the capture endpoint

  • Require authentication if captures are user-specific or consume a shared account allowance.
  • Authorize who can capture which domains and access each result.
  • Apply per-user and global rate limits. A capture endpoint can be abused to create unwanted remote browsing work.
  • Keep Django’s normal CSRF protections for browser-originated unsafe requests.
  • Set request-body limits and bound URL length, viewport dimensions, waits, and output size.
  • Redact API keys, cookies, authorization headers, and sensitive target URLs from logs.
  • Consider whether submitted URLs or captures contain personal or confidential data before retaining or exposing them.

8. Reliability, performance, and cost

A remote browser render can take longer than an ordinary database-backed view. Set explicit connection and read timeouts, handle timeouts and network exceptions, and return a controlled gateway error rather than leaking provider internals. Validate response status and content type before returning data. Set an application-level response-size limit if the client library and provider flow permit it.

For interactive use, decide how long the caller can wait. For slow or repeated captures, enqueue work in a task queue and expose a job status endpoint; cache equivalent results only when the URL, options, authorization context, and freshness requirements match. A cache must not accidentally serve one user’s private capture to another.

Cost depends on the chosen provider’s current pricing and billing rules, which should be checked directly. Do not assume failed captures, cache hits, or retries are free unless the provider explicitly says so. Add usage controls and avoid automatic retry loops: retries can duplicate work or charges. No latency, throughput, or quota guarantee is established by the example API contract discussed here.

9. Troubleshooting

Symptom Likely cause Fix
Django returns 400 Malformed JSON, disallowed host, unsupported format, or invalid options Check the request body and allowlist; return field-specific validation errors where safe.
Django returns 500 saying service is not configured The deployment does not provide the secret environment variable Set the key in the deployment’s secret configuration and restart or reload the application as required.
Django returns 502 Provider network failure, non-success status, or unexpected response mode Check sanitized server logs and provider status details; verify endpoint, auth header, and configured response mode.
Image opens as JSON or download fails The provider’s default response is JSON or the response media type is wrong Request its documented bytes mode or parse the documented JSON result shape.
Browser reports a broken image Wrong media type, truncated body, or a PDF returned to an image element Validate content type and return a matching format and disposition.
Capture is blank or incomplete Target page failed, content is delayed, or the page needs a wait condition Check the URL and provider error details; use a bounded, relevant wait option and test full-page behavior.
Request hangs or times out No explicit timeout or render duration exceeds the synchronous request budget Set connect/read timeouts and move long-running captures to a background job.
Private network or internal page is reachable Target URL policy is too permissive or redirects bypass validation Enforce host and destination-address restrictions across redirects, and block private/reserved destinations at an appropriate network boundary.
Unexpected usage or duplicate captures Unbounded caller traffic or retries without idempotency/caching policy Add authentication, rate limits, usage tracking, and deliberate retry behavior.

10. Django screenshot tests versus a hosted API

Use Django’s Selenium screenshot support to inspect your own admin UI during tests. Django 6.0 documents the --screenshots option, which writes images to tests/screenshots/, and @screenshot_cases for desktop, mobile, small-screen, RTL, dark, and high-contrast cases. This requires a browser-based test setup and produces test diagnostics.

Use a hosted screenshot API when the application needs runtime URL capture as a feature. Consider the external service dependency, the target pages, and where capture data is processed. These workflows solve different problems.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request can return a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo site and the API documentation.

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}`);

Keep the access key in Django’s server-side configuration. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.

Sign up free for 1,000 screenshots a month—no card required.

FAQ

How do I capture a website screenshot from Django?

Make a server-side request to a screenshot API from a view or worker, validate the target and options, then return verified image/PDF bytes or a permitted result URL.

Should I use a screenshot API or Django Selenium screenshots?

Use Selenium screenshots for test and visual diagnostics of your Django UI. Use a hosted API when URL capture is a feature available to application users.

Can I put the screenshot API key in frontend JavaScript?

No. Keep it server-side and have the browser call your authenticated Django endpoint.

Does Django’s ALLOWED_HOSTS secure the target URL?

No. It validates incoming Host values for Django. Your application must separately validate destinations fetched by a remote screenshot service.