How to Use a Screenshot API in a Django App in India
Call a screenshot service safely from Django, return or store the image, and handle timeouts, URL validation, and provider errors. Includes a Playwright option and a one-call hosted API.
A Django app can use a screenshot API by making a server-side HTTP request to the provider, then returning the resulting image, storing it, or passing on a controlled image URL. Keep the API key on the server, validate the target URL, set timeouts, and follow the provider’s documented request and response format. API contracts vary: some return image bytes, while others return JSON containing a screenshot URL.
This guide shows a production-oriented Django pattern, a self-managed Playwright alternative, and a hosted option. Provider availability, pricing, data handling, and data location in India vary; check the current provider documentation and terms before choosing a service.
1. Choose the response flow
Before writing the view, decide what your application should do with the captured image:
| Flow | Use it when | Trade-off |
|---|---|---|
| Return image bytes | The caller needs the image immediately and captures finish within your request budget. | The Django request stays open while the provider renders the page. |
| Store the image and return your own URL | You need durable access, caching, or control over who can view the result. | You must manage storage, retention, and access controls. |
| Return a provider URL | The provider returns a URL and its access and retention behavior suits your application. | Availability and access may depend on the provider’s retention and URL rules. |
| Queue a job | Captures may be slow, numerous, or unsuitable for a synchronous web request. | You need a task queue and a status or result endpoint. |
Do not assume a provider returns PNG bytes just because the requested format is PNG. Read the response contract, check the HTTP status and content type, and validate JSON fields or image bytes before sending them to a client.
2. Install and configure the Django client
The example below uses requests. Install it in the project’s environment:
python -m pip install requests
Keep the provider key in deployment configuration or a secret store. For local development, set an environment variable in your shell or use your project’s existing secret management approach:
export SCREENSHOT_API_KEY='your-provider-key'
Do not put a production key in a template, static JavaScript, a committed settings file, or a URL users can inspect. Use the provider’s supported authentication mechanism. When the provider supports header authentication, prefer it to placing a secret in query parameters, which can be recorded in logs.
3. Add URL validation and a synchronous capture view
A public endpoint that accepts any target URL can be abused to make your server request internal services or consume excessive resources. If your product only needs a known set of sites, use an allowlist. The example accepts only HTTPS URLs whose hostname is in a configured list. Adapt the list to your product; do not remove validation without another explicit access policy.
# settings.py
import os
SCREENSHOT_API_KEY = os.environ.get("SCREENSHOT_API_KEY", "")
SCREENSHOT_API_URL = os.environ.get("SCREENSHOT_API_URL", "")
SCREENSHOT_ALLOWED_HOSTS = {
"example.com",
"www.example.com",
}
SCREENSHOT_API_URL is deliberately deployment configuration: use the exact endpoint in the chosen provider’s current documentation. The following view is a provider-neutral template. Replace its request method, authentication, payload, and response parsing with that provider’s documented contract.
# views.py
from urllib.parse import urlsplit
import requests
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import require_GET
ALLOWED_FORMATS = {"png": "image/png", "jpeg": "image/jpeg", "webp": "image/webp"}
def validate_target_url(value):
if not value or len(value) > 2048:
return False
try:
parsed = urlsplit(value)
host = (parsed.hostname or "").lower().rstrip(".")
return (
parsed.scheme == "https"
and host in settings.SCREENSHOT_ALLOWED_HOSTS
and parsed.username is None
and parsed.password is None
)
except ValueError:
return False
@require_GET
def capture(request):
target_url = request.GET.get("url", "")
image_format = request.GET.get("format", "png").lower()
if not validate_target_url(target_url):
return JsonResponse({"error": "url must be an allowed HTTPS URL"}, status=400)
if image_format not in ALLOWED_FORMATS:
return JsonResponse({"error": "format must be png, jpeg, or webp"}, status=400)
if not settings.SCREENSHOT_API_KEY or not settings.SCREENSHOT_API_URL:
return JsonResponse({"error": "screenshot service is not configured"}, status=503)
try:
# Replace this request with the provider's documented method and payload.
upstream = requests.post(
settings.SCREENSHOT_API_URL,
headers={"Authorization": f"Bearer {settings.SCREENSHOT_API_KEY}"},
json={"url": target_url, "format": image_format},
timeout=(5, 45), # connection timeout, read timeout
)
except requests.Timeout:
return JsonResponse({"error": "screenshot service timed out"}, status=504)
except requests.RequestException:
return JsonResponse({"error": "could not reach screenshot service"}, status=502)
if upstream.status_code == 429:
return JsonResponse({"error": "screenshot service rate limit reached"}, status=429)
if not upstream.ok:
# Do not expose provider response bodies or credentials to the caller.
return JsonResponse({"error": "screenshot service returned an error"}, status=502)
# This branch assumes the provider contract returns raw image bytes.
expected_type = ALLOWED_FORMATS[image_format]
actual_type = upstream.headers.get("Content-Type", "").split(";", 1)[0].lower()
if actual_type != expected_type:
return JsonResponse({"error": "screenshot service returned an unexpected content type"}, status=502)
response = HttpResponse(upstream.content, content_type=expected_type)
response["Content-Disposition"] = f'inline; filename="screenshot.{image_format}"'
response["Cache-Control"] = "private, no-store"
return response
The example is intentionally explicit about its assumptions. It expects a POST endpoint, Bearer authentication, JSON input, and raw image bytes. Those details are not universal. If the provider returns JSON, parse and validate the documented field instead of treating the JSON body as an image. If it returns a URL, decide whether to redirect, proxy, or store the image, and apply your own access and retention policy.
Register the view in your URL configuration:
# urls.py
from django.urls import path
from .views import capture
urlpatterns = [
path("api/screenshot", capture, name="capture"),
]
A caller can request a capture like this:
curl --get 'https://your-app.example/api/screenshot' \
--data-urlencode 'url=https://www.example.com/' \
--data-urlencode 'format=png' \
--output screenshot.png
For a public application, authenticate and rate-limit this endpoint. An allowlist limits destinations, but it does not replace application authorization or abuse controls.
4. Adapt request and response handling to the provider
Read the provider’s API reference for these contract details before deployment:
- HTTP method and endpoint: use the documented route and method.
- Authentication: use the supported header or other documented mechanism; keep the secret server-side.
- Input: check how the provider names the target URL, format, viewport, and other options.
- Output: determine whether a success response contains raw bytes, JSON, a URL, or a job identifier.
- Errors and limits: handle authentication failures, invalid parameters, rate limits, provider errors, and response timeouts.
- Capture controls: confirm supported output formats, viewport dimensions, full-page behavior, and any advanced options. These vary by provider.
For a JSON response, validate its structure and the expected URL scheme and host before using the returned URL. Avoid blindly redirecting a user to an arbitrary value supplied by an upstream response. For asynchronous jobs, return a job identifier and provide a separate authenticated status or result endpoint.
5. Use Playwright when you want to manage the browser
A hosted API is not required for every screenshot workflow. Playwright can run a browser in your own environment, which gives your team control over browser setup and execution. You then own browser installation, resource use, concurrency, and maintenance. Its Python documentation covers [page screenshots](https://playwright.dev/python/docs/screenshots), including full-page and element captures.
Install Playwright and its Chromium browser in an environment where browser dependencies are available:
python -m pip install playwright
python -m playwright install chromium
A small Python capture function can be called from a background task or another controlled execution path:
from playwright.sync_api import sync_playwright
def capture_with_playwright(url, output_path="screenshot.png"):
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1440, "height": 900})
page.goto(url, wait_until="networkidle", timeout=45_000)
page.screenshot(path=output_path, full_page=True)
browser.close()
return output_path
networkidle is not suitable for every site: analytics, streaming pages, or long-lived connections can prevent it from completing. Depending on the page, use a specific readiness signal such as a selector, or a documented wait condition. Validate URLs with the same care as for a hosted provider. Do not launch an unbounded number of browsers inside web workers.
Django’s Playwright-related test support is for testing Django interfaces; it is distinct from building a production screenshot endpoint. See the [Django testing documentation](https://docs.djangoproject.com/en/stable/topics/testing/tools/) when the need is screenshot-based UI tests.
6. Add reliability for production traffic
Timeouts and request budgets
Set both connection and read timeouts. The sample uses 5 seconds to connect and 45 seconds to read; tune these values to the provider’s documented behavior and your application’s request budget. A capture that exceeds your web server or reverse-proxy timeout can fail even if the provider eventually finishes.
Retries and duplicate work
Do not retry every failure automatically. A timeout can happen after the provider has already started or completed the capture, so a retry may duplicate work. Retry only transient failures, use a small bounded retry policy with backoff, and use an idempotency mechanism if the provider documents one. Avoid retrying invalid input, authentication failures, or rate-limit responses without honoring the provider’s reset guidance.
Queue long or bulk captures
For slow pages, large batches, or user-facing flows that must stay responsive, enqueue a task and return a job ID. Store task state and expose an authenticated status route. If the provider supports asynchronous jobs or batch capture, check its limits, callback authentication, and failure semantics. Do not assume those features exist for every API.
Validate content and cap resource use
Check response status, content type, and a maximum acceptable response size before relaying or storing image bytes. Avoid trusting client-supplied content types. Apply per-user quotas and request limits so that one caller cannot consume all provider capacity.
7. Security, privacy, and India-specific checks
- Prevent SSRF: restrict destination hosts or apply an equivalent allowlist policy. Consider redirects: a permitted hostname can redirect elsewhere. Enforce destination rules at the provider or network layer where possible.
- Protect credentials: keep the key in server configuration, rotate it through your secret-management process, and never return it in errors.
- Protect captured content: screenshots can include personal, private, or authenticated page data. Decide who may request captures, who can view results, and how long results remain available.
- Check processing location: the research available for this guide does not establish any provider’s India-specific availability, data location, or retention policy. Confirm those details directly with the provider before sending sensitive pages.
- Control logs: avoid logging secrets and consider whether full target URLs contain sensitive query parameters.
8. Performance and cost
Capture duration depends on the target page, its assets, wait condition, viewport, and provider or browser environment. No comparative latency benchmark is established here. Measure your own pages and traffic pattern before choosing synchronous versus queued capture.
With a hosted service, check the current quota, billing unit, overage behavior, and whether failed captures or cache hits are charged. Those rules differ by provider and can change. For a self-managed Playwright deployment, include compute, memory, browser processes, storage, and maintenance in the operational cost. Use caching only when the page’s freshness and privacy requirements allow it.
Or skip the browser setup
ScreenshotNeo provides a screenshot API and MCP server for Django applications. One GET request returns a screenshot or PDF; see the ScreenshotNeo API documentation for the API contract and options.
For a PNG capture, Django can request the image bytes directly:
import os
import requests
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import require_GET
@require_GET
def screenshotneo_capture(request):
target_url = request.GET.get("url", "")
if not target_url:
return JsonResponse({"error": "url is required"}, status=400)
# Apply your application's destination allowlist and authorization here.
try:
result = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": os.environ["SCREENSHOTNEO_API_KEY"],
"url": target_url,
"format": "png",
},
timeout=90,
)
except requests.Timeout:
return JsonResponse({"error": "screenshot request timed out"}, status=504)
except requests.RequestException:
return JsonResponse({"error": "screenshot service could not be reached"}, status=502)
if not result.ok:
return JsonResponse({"error": "screenshot request failed"}, status=502)
return HttpResponse(result.content, content_type="image/png")
Equivalent direct requests:
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}`);
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 or 403 from provider | Missing, invalid, or insufficiently authorized key. | Check the server secret and the provider’s required authentication format; do not expose the key to the browser. |
| 400 from provider | Wrong parameter name, unsupported format, malformed URL, or request body that does not match the provider contract. | Compare the request with current provider documentation and validate options before sending. |
| 429 response | Provider quota or rate limit reached. | Honor the provider’s reset guidance, apply per-user limits, and queue work where appropriate. |
| Django returns 502 | Provider returned an error, or the response shape/content type did not match the view’s assumption. | Inspect sanitized server-side diagnostics and update the parser to match the documented success response. |
| Django returns 504 | Capture exceeded the read timeout or upstream request budget. | Review page readiness behavior and timeout budgets; move long jobs to a queue. |
| Image appears blank or incomplete | The page may require more rendering time, block automated browsing, lazy-load content, or fail to load assets. | Use a provider-supported wait option or selector, inspect the page verdict if available, and test the target page directly. |
| Playwright hangs at network idle | The page keeps network connections open or continually fetches resources. | Wait for a specific selector or another page-ready condition rather than network idle. |
| Local Playwright works, production fails | Browser dependencies, memory, permissions, or process limits differ in deployment. | Install the required browser and operating-system dependencies in the deployment image and bound concurrency. |
| Unexpected internal destination risk | User-controlled URL or redirect bypasses simple input checks. | Restrict hosts, handle redirects carefully, and use network egress controls for sensitive deployments. |
FAQ
Should the browser call the screenshot provider directly?
Usually no for a private API key. Call from Django so the credential stays server-side, then authorize access to your Django endpoint.
Can I return a screenshot as a Django file download?
Yes. Return the validated bytes with a suitable content type and a controlled Content-Disposition header, or store the file and serve it through an access-controlled route.
Is Playwright suitable for production screenshots?
It can be, if your deployment manages browsers, dependencies, concurrency, timeouts, and resource use. A hosted API reduces browser operations work but introduces provider limits, terms, and data-processing considerations.
Does using a provider guarantee India data residency?
No such conclusion is supported here. Ask the provider where requests and captured page data are processed, how long results are retained, and what controls apply to your account.


