ScreenshotNeo

BlogHow-to

How to Screenshot a Webpage Hosted on an Indian Intranet with an API

An API can screenshot an intranet page only if its rendering browser can reach the network and authenticate. Here is a self-hosted Python example, security guidance, and a clear way to choose where it should run.

By the ScreenshotNeo team4 October 202612 min read

Direct answer: An API can screenshot an intranet webpage only when the browser that renders it can reach that page and authenticate to it. For a private Indian company network, that usually means running an approved browser worker inside the reachable network or establishing an approved private connection to a managed renderer. Supplying a password or cookie does not create a network route to a private hostname.

The country where the intranet is hosted does not by itself change this networking requirement. Your organization still needs to decide where the browser runs, what credentials it can use, and whether the captured image may leave the organization-controlled environment.

1. Check reachability before choosing an API

Test from the environment that will run the rendering browser—not just from a developer laptop connected to the company VPN. Check that the hostname resolves using the expected DNS, that the route and firewall allow the connection, and that the page loads over the required port.

  1. From an approved machine in the intended network, resolve the intranet hostname and open the page.
  2. Identify network dependencies: VPN, private DNS, firewall rules, proxy, device posture, or a source-IP restriction.
  3. Identify identity dependencies: session cookie, HTTP Basic Authentication, authorization header, SSO redirect, MFA, or client certificate.
  4. Choose where the browser worker will run. For a private hostname, an organization-managed worker in the reachable network is the most direct pattern.
  5. Check the resulting screenshot for incomplete rendering or information that should not be stored or shared.

A managed screenshot endpoint may support navigation, cookies, Basic Authentication, or custom headers, but that does not prove that its hosted browser can route to your private DNS or network. Verify the chosen deployment’s route, identity compatibility, and data handling with your network and security owners before sending internal content. Cloudflare documents screenshot controls and authentication options; those feature descriptions do not guarantee access to a particular intranet. Cloudflare screenshot API documentation.

2. Choose where the rendering browser runs

Organization-hosted browser worker

Run a small API service and browser in a network location that can access the intranet. The service can open an allowlisted page using an approved service identity or session and return the rendered image through a controlled endpoint. This is usually the practical fit when external services cannot route to the private hostname.

  • Keep the worker on an approved network segment and limit its outbound destinations.
  • Use a narrowly privileged identity. Store credentials in an approved secret store and do not log them.
  • Restrict who can call the capture endpoint and which hosts it can visit.
  • Decide whether images, logs, and temporary browser data may leave the network and how long they are retained.
  • Keep the browser and its dependencies patched, and set limits on concurrent jobs and capture duration.

Managed screenshot API

A managed renderer is an option only if its documented deployment and network route satisfy your organization’s requirements. Confirm private DNS and routing, the supported authentication flow, and the handling of both credentials and resulting screenshots. A vendor example using an internal-looking hostname is not evidence that your organization’s DNS, firewall, VPN, or SSO will work with that service.

User-mediated browser capture

For an occasional one-off, a person who is already authorized to open the page can capture a browser tab or window. The Screen Capture API prompts the user to select a source and requires user interaction; review the result because other visible content may be included. It is not an unattended server-side API workflow. MDN: getDisplayMedia().

3. Build a small self-hosted screenshot API in Python

This example runs Playwright in the same network environment as the intranet. It exposes one fixed, allowlisted page rather than accepting an arbitrary URL, requires a bearer token, waits for a known page element, and returns a PNG. Adapt the hostname, readiness selector, and approved authentication to your environment.

The fixed target is a deliberate security boundary. An endpoint that accepts arbitrary URLs can be abused to make the browser query unrelated internal systems. OWASP recommends layered SSRF defenses, including destination restrictions and network controls. OWASP SSRF Prevention Cheat Sheet.

Install

python -m venv .venv
. .venv/bin/activate
pip install fastapi uvicorn playwright
playwright install chromium

Set these environment variables in the worker’s deployment environment. Use your organization’s secret manager for the bearer token and any session cookie; do not commit them to source control.

export CAPTURE_TOKEN='replace-with-a-long-random-secret'
export INTRANET_URL='https://portal.internal.example/dashboard'
export READY_SELECTOR='main'
# Optional, if the application uses a session cookie:
export SESSION_COOKIE_NAME='sessionid'
export SESSION_COOKIE_VALUE='replace-with-session-value'
export INTRANET_COOKIE_DOMAIN='portal.internal.example'

Create app.py

import os
import secrets
from urllib.parse import urlparse

from fastapi import FastAPI, Header, HTTPException, Response
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError

app = FastAPI()

TARGET_URL = os.environ["INTRANET_URL"]
READY_SELECTOR = os.getenv("READY_SELECTOR", "main")
CAPTURE_TOKEN = os.environ["CAPTURE_TOKEN"]

parsed = urlparse(TARGET_URL)
if parsed.scheme != "https" or not parsed.hostname:
    raise RuntimeError("INTRANET_URL must be an HTTPS URL with a hostname")
ALLOWED_HOST = parsed.hostname.lower()


@app.get("/capture.png")
async def capture(authorization: str | None = Header(default=None)):
    expected = f"Bearer {CAPTURE_TOKEN}"
    if authorization is None or not secrets.compare_digest(authorization, expected):
        raise HTTPException(status_code=401, detail="Unauthorized")

    browser = None
    try:
        async with async_playwright() as p:
            browser = await p.chromium.launch(headless=True)
            context = await browser.new_context(
                viewport={"width": 1440, "height": 1000},
                device_scale_factor=1,
            )
            cookie_name = os.getenv("SESSION_COOKIE_NAME")
            cookie_value = os.getenv("SESSION_COOKIE_VALUE")
            cookie_domain = os.getenv("INTRANET_COOKIE_DOMAIN", ALLOWED_HOST)
            if cookie_name and cookie_value:
                await context.add_cookies([{
                    "name": cookie_name,
                    "value": cookie_value,
                    "domain": cookie_domain,
                    "path": "/",
                    "secure": True,
                    "httpOnly": True,
                    "sameSite": "Lax",
                }])

            page = await context.new_page()
            response = await page.goto(
                TARGET_URL,
                wait_until="domcontentloaded",
                timeout=45000,
            )
            if response is None or response.status >= 400:
                status = response.status if response else "no response"
                raise HTTPException(status_code=502, detail=f"Page navigation failed: {status}")

            # Reject redirects to another host. Add approved redirect hosts explicitly
            # if your login flow legitimately needs them.
            final_host = (urlparse(page.url).hostname or "").lower()
            if final_host != ALLOWED_HOST:
                raise HTTPException(status_code=502, detail="Navigation left the allowed host")

            try:
                await page.locator(READY_SELECTOR).wait_for(state="visible", timeout=20000)
            except PlaywrightTimeoutError:
                raise HTTPException(status_code=504, detail="Page readiness selector timed out")

            png = await page.screenshot(full_page=True, type="png", animations="disabled")
            return Response(content=png, media_type="image/png")
    except HTTPException:
        raise
    except PlaywrightTimeoutError:
        raise HTTPException(status_code=504, detail="Browser navigation timed out")
    except Exception:
        # Log a request ID and sanitized error details in production; never log secrets.
        raise HTTPException(status_code=502, detail="Browser capture failed")
    finally:
        if browser is not None:
            await browser.close()

Start the service on a private interface or behind your organization’s authenticated ingress:

uvicorn app:app --host 0.0.0.0 --port 8000

The example checks that the final page remains on the configured hostname. If your legitimate SSO flow redirects to a separate identity host, do not remove that check without replacing it with an explicit redirect-host allowlist and review. The example cookie is only an illustration of a session-cookie flow: cookie domain, security attributes, lifetime, and rotation must match the application and organization policy. It does not implement interactive SSO, MFA, client certificates, or device posture.

Call the worker

Run these commands from a client that can reach the worker’s API endpoint. Replace the address and token with your deployment values.

cURL

curl --fail --silent --show-error \
  -H 'Authorization: Bearer YOUR_CAPTURE_TOKEN' \
  'https://capture.internal.example/capture.png' \
  -o intranet.png

Python

import requests

r = requests.get(
    "https://capture.internal.example/capture.png",
    headers={"Authorization": "Bearer YOUR_CAPTURE_TOKEN"},
    timeout=60,
)
r.raise_for_status()
with open("intranet.png", "wb") as image:
    image.write(r.content)

Node.js

const res = await fetch("https://capture.internal.example/capture.png", {
  headers: { Authorization: "Bearer YOUR_CAPTURE_TOKEN" },
  signal: AbortSignal.timeout(60_000),
});
if (!res.ok) throw new Error(`Capture failed: ${res.status} ${await res.text()}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await (await import("node:fs/promises")).writeFile("intranet.png", bytes);

4. Configure authentication without confusing it with reachability

First establish that the worker can resolve and route to the page. Then choose an authentication mechanism that the application actually supports.

  • Session cookie: Use an approved service session or a securely provisioned cookie when the application supports it. Cookies expire and may be bound to other session state, so plan rotation and failure handling.
  • HTTP Basic Authentication: Use only if the application explicitly supports it and the connection is protected by HTTPS. A Basic Auth option cannot complete an interactive identity-provider flow by itself.
  • Authorization header: A bearer or other custom header works only if the application accepts that credential on the requested route. Do not attach it to unrelated hosts or redirects.
  • SSO and MFA: Do not assume a screenshot worker can complete an interactive login. Confirm a supported service identity or approved automation flow with the identity team; do not bypass MFA or device checks.
  • VPN or private link: This is a routing decision, not a credential. The network owner must approve the route and firewall policy for the worker.

Cloudflare’s documentation describes session cookies, Basic Authentication, and custom authorization headers as available mechanisms for its browser rendering flow. Whether those mechanisms work for your page depends on the actual network path and identity setup. Cloudflare screenshot API options.

5. Tune capture timing and dimensions

A navigation event does not always mean a JavaScript application has finished rendering. Prefer waiting for a stable element that appears when the content you need is ready. The sample waits for main; change it to a page-specific selector such as [data-testid="report-ready"] when available.

  • Readiness: Wait for a meaningful visible selector. Use a short fixed delay only when the page has no stable readiness signal.
  • Full page: full_page=True captures the full document, which can create very tall images and higher memory use. Use a viewport screenshot when only the visible region is needed.
  • Viewport: Set width and height to match the target layout. Responsive pages may show different content at different sizes.
  • Device scale: Increase device scale factor for sharper output, understanding that pixel dimensions and memory use rise.
  • Lazy-loaded content: If images load only while scrolling, scroll through the page before capture and wait for the specific content. Confirm that this does not trigger unwanted actions.
  • Timeouts: Set separate navigation and readiness limits. A timeout should fail the job clearly rather than silently returning a partial image.

Cloudflare’s documented screenshot controls include viewport, selector, full-page, wait-for-selector, and wait timing options; its documentation also notes that scripts may render after the initial page load. Screenshot parameters.

6. Secure the endpoint against misuse

A screenshot worker has the network access of the machine it runs on. If callers can choose arbitrary URLs, the worker may be turned into a proxy for internal services or machine-local endpoints. Treat the capture API as a privileged service.

  • Prefer a fixed target or an explicit host and path allowlist. Do not accept arbitrary schemes, ports, or URLs from untrusted callers.
  • Apply network egress rules as a second layer; application checks alone are not enough.
  • Validate redirects and resolved destinations. Do not let an approved URL redirect the browser to an unapproved internal host.
  • Use a low-privilege identity with access only to the page or data needed for the capture.
  • Authenticate callers, rate-limit requests, cap image dimensions and concurrency, and record sanitized audit events.
  • Keep credentials out of URLs, screenshots, logs, exceptions, and source control. Restrict access to saved images because they may contain internal data.

See OWASP’s SSRF guidance for destination validation and layered network defenses.

7. Troubleshooting

Symptom Likely cause Fix
Hostname does not resolve The worker uses public DNS or is outside the network that knows the private zone. Run from the approved network and configure the organization’s intended DNS resolver. Test from the worker environment.
Connection refused or times out No route, firewall denial, wrong port, unavailable VPN, or proxy requirement. Ask the network owner to verify the worker’s route and egress policy. Credentials will not fix a routing failure.
Screenshot shows a login page Session missing or expired, unsupported SSO/MFA, cookie scope mismatch, or auth header not accepted. Confirm the supported automation identity and inspect the final URL and page status without logging secrets. Re-provision or rotate the approved session if applicable.
Redirect-host error The application redirected to an identity provider or another host. Review the expected flow. Add only explicitly approved hosts to a redirect allowlist and ensure credentials are not forwarded to unrelated hosts.
Image is blank or incomplete JavaScript has not rendered the content, selector is wrong, or the page is waiting on an API call. Wait for a stable content selector, inspect browser console/network errors in a controlled environment, and adjust timeout after identifying the dependency.
Images are missing lower on the page Content is lazy-loaded only when scrolled into view. Scroll through the page before capture, wait for images or a page-specific readiness marker, then capture.
401 from the capture service Missing or incorrect bearer token. Check the caller’s authorization header and the deployed secret; do not print either value to logs.
502 from the capture service Browser launch, navigation, or upstream page failure. Check sanitized service logs, browser installation, DNS, TLS trust, and the intranet’s response status.
504 or readiness timeout Navigation or the selected element exceeded its timeout. Verify the selector exists in the rendered page, identify slow dependencies, and tune the appropriate timeout rather than adding an arbitrary long delay.
Browser can load page but browser JavaScript cannot call another local service Browser-originated local-network access policy, CORS, or application configuration. Distinguish the browser’s own server-side navigation route from requests initiated by page JavaScript. Review current browser permission and application policies; do not rely on obsolete preflight headers.

Chrome’s current Local Network Access documentation describes permission checks for browser requests from public contexts to local-network destinations. That is distinct from whether a server-side worker itself has a route to the intranet. Chrome Local Network Access overview.

8. Performance, reliability, and cost

  • Performance: Browser startup, page JavaScript, fonts, images, and network latency usually dominate capture time. Reuse browser processes carefully if throughput warrants it, but isolate browser contexts and credentials between jobs.
  • Memory: Full-page and high-resolution captures use more memory. Limit viewport, scale, page height, parallel jobs, and output size to match worker capacity.
  • Reliability: Make jobs bounded and observable. Record status, duration, final hostname, and failure category without recording secrets or page contents. Retry only transient failures, with a limit and backoff; do not blindly retry authentication or policy failures.
  • Network dependency: Worker availability depends on internal DNS, routing, firewall policy, and the target application. Monitor these separately from the screenshot code.
  • Cost: A self-hosted worker uses your organization’s compute and operational time. The research sources do not establish a comparable vendor price, service level, or India-specific residency guarantee; verify those directly for any managed option.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. Its public renderer should not be assumed to reach an Indian organization’s private intranet: use it for a target only when that target is reachable by the renderer and your organization approves the data path. For reachable pages, one GET request returns an image; see the ScreenshotNeo 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

ScreenshotNeo removes cookie and consent 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; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Can I pass an internal URL to any screenshot API?

Only if that API’s renderer can resolve and route to the private host and your organization permits the data flow. An accepted URL parameter alone does not provide network access.

Will a bearer token make an intranet page reachable?

No. A token can authenticate a request to an application that the renderer can already reach; it does not establish DNS, VPN, firewall access, or a private route.

Can an API reliably handle an interactive SSO and MFA prompt?

Do not assume so. Confirm an organization-approved automation identity and supported flow with the identity team; the mechanisms available depend on the application and identity provider.

Does this workflow have a special India-only API requirement?

The networking and rendering requirements are general. Your organization must separately assess its contractual, security, and data-handling requirements; this guide does not determine applicable legal obligations.