ScreenshotNeo

BlogHow-to

Full-Page Screenshot Tools That Work With Pages Behind a Login

Capture a full page after signing in with Playwright, reuse saved authentication safely, and choose the right workflow for private pages.

By the ScreenshotNeo team4 October 202611 min read

To capture a full-page screenshot of a page behind a login, use a browser automation tool that can sign in or load an authenticated browser session, then capture the page with full-page mode enabled. Playwright supports both parts: its browser context can reuse saved authentication state, and page.screenshot({ fullPage: true }) captures the full scrollable page. These are separate capabilities composed in the same browser context. A screenshot API that receives only a URL cannot be assumed to have access to your private browser session.

This guide uses Playwright with Node.js and Python. It covers a one-time interactive sign-in and the reusable saved-state workflow. A valid session is still required; multi-factor authentication (MFA), single sign-on (SSO), bot checks, and site-specific login flows can need extra handling.

1. Choose a workflow for the authenticated session

There are two common ways to make the page available to the capture browser:

  • Sign in in the automation browser: useful for a one-off capture or when you need to exercise the login flow. You can sign in manually in the opened browser, or automate the form if the site permits it.
  • Load saved browser state: useful for repeat captures. Sign in once, save the context state, and load it into later browser contexts until it expires.

Playwright storage state can include cookies and local storage, and can include IndexedDB when requested. Session storage is not persisted by the standard storage-state file. Treat saved state as a credential: anyone who obtains usable cookies or tokens may be able to impersonate that account. Keep it out of source control, restrict access, and rotate or delete it when no longer needed. See the official Playwright authentication guide.

2. Set up Playwright

The following examples use Playwright’s Node.js library and Python package. Install the package and its browser before running a script. The examples navigate to a placeholder protected URL; replace it with the page you are authorized to access.

Node.js installation

npm init -y
npm install playwright
npx playwright install chromium

Python installation

python -m pip install playwright
python -m playwright install chromium

The Playwright browser runs locally under your control. The script needs network access to the target site and permission to sign in to the account.

3. Sign in interactively, then capture the full page

This Node.js script opens a visible Chromium window so you can complete a site’s normal login flow, including a manual MFA step where applicable. Once you press Enter in the terminal, it navigates to the protected page and saves the complete scrollable page as a PNG.

// save as capture.mjs
import { chromium } from 'playwright';
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

const loginUrl = 'https://example.com/login';
const protectedUrl = 'https://example.com/account/report';

const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
const page = await context.newPage();

try {
  await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
  const rl = createInterface({ input, output });
  await rl.question('Complete sign-in in the browser, then press Enter here. ');
  rl.close();

  await page.goto(protectedUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
  // Replace this with a locator that proves the expected signed-in page loaded.
  await page.locator('main').waitFor({ state: 'visible', timeout: 30_000 });
  await page.screenshot({ path: 'private-page.png', fullPage: true });
} finally {
  await browser.close();
}

Run it with node capture.mjs. The main locator is only an example readiness check. Substitute a page-specific selector, heading, or other condition that confirms you reached the intended authenticated content. If login redirects to a different URL, the script still proceeds after you press Enter; the check on the protected page helps catch an expired or unsuccessful sign-in.

Python equivalent

# save as capture.py
from playwright.sync_api import sync_playwright

login_url = "https://example.com/login"
protected_url = "https://example.com/account/report"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    context = browser.new_context(viewport={"width": 1440, "height": 1000})
    page = context.new_page()
    try:
        page.goto(login_url, wait_until="domcontentloaded", timeout=60_000)
        input("Complete sign-in in the browser, then press Enter here. ")

        page.goto(protected_url, wait_until="domcontentloaded", timeout=60_000)
        # Replace this with a locator that proves the expected signed-in page loaded.
        page.locator("main").wait_for(state="visible", timeout=30_000)
        page.screenshot(path="private-page.png", full_page=True)
    finally:
        browser.close()

Run it with python capture.py. For a headless server, first establish and save a valid session using an interactive run, then use the saved-state workflow below.

4. Save and reuse an authenticated session

For repeat captures, save context state after a successful login. Store the file in a private location and add it to .gitignore. Do not commit the state file, even to a private repository. This Node.js setup script pauses while you complete sign-in, then writes the session state.

// save as save-auth.mjs
import { chromium } from 'playwright';
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
try {
  await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
  const rl = createInterface({ input, output });
  await rl.question('Complete sign-in, then press Enter here. ');
  rl.close();
  await context.storageState({ path: 'auth-state.json' });
} finally {
  await browser.close();
}
# Keep authentication state out of Git
printf '\nauth-state.json\n' >> .gitignore
node save-auth.mjs

Then create a fresh context from that state and capture. This script checks that the expected protected content is present before taking the screenshot.

// save as capture-saved.mjs
import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext({
  storageState: 'auth-state.json',
  viewport: { width: 1440, height: 1000 },
});
const page = await context.newPage();
try {
  await page.goto('https://example.com/account/report', {
    waitUntil: 'domcontentloaded',
    timeout: 60_000,
  });
  await page.locator('main').waitFor({ state: 'visible', timeout: 30_000 });
  await page.screenshot({ path: 'private-page.png', fullPage: true });
} finally {
  await browser.close();
}

If your site uses IndexedDB for its authentication token, save it explicitly with await context.storageState({ path: 'auth-state.json', indexedDB: true }) in supported Playwright versions. For Python, the equivalent is context.storage_state(path="auth-state.json", indexed_db=True). Confirm the option exists in the installed version’s API reference.

Python saved-state workflow

# Save after signing in interactively:
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    context = browser.new_context()
    page = context.new_page()
    page.goto("https://example.com/login", wait_until="domcontentloaded")
    input("Complete sign-in, then press Enter here. ")
    context.storage_state(path="auth-state.json")
    browser.close()

# In a separate capture script, load the saved state:
with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context(
        storage_state="auth-state.json",
        viewport={"width": 1440, "height": 1000},
    )
    page = context.new_page()
    try:
        page.goto(
            "https://example.com/account/report",
            wait_until="domcontentloaded",
            timeout=60_000,
        )
        page.locator("main").wait_for(state="visible", timeout=30_000)
        page.screenshot(path="private-page.png", full_page=True)
    finally:
        browser.close()

Playwright documents loading saved state into browser contexts and warns that state files can contain sensitive cookies and headers. See authentication and state reuse and the Page API screenshot options.

5. Capture readiness and full-page options

fullPage: true changes capture scope: it captures the full scrollable page rather than only the visible viewport. It defaults to false. It does not sign in, wait for an application-specific state, expand collapsed sections, or guarantee that lazy content has loaded.

Need Playwright approach What to watch
Full scrollable page page.screenshot({ fullPage: true }) Very tall pages can require substantial browser memory.
Wait for the page shell page.locator('main').waitFor() Choose a selector specific to the intended page.
Wait for a known URL page.waitForURL('**/account/report') Useful after a known redirect or navigation.
Wait for a specific element page.getByRole('heading', { name: 'Report' }).waitFor() Prefer a meaningful readiness signal over a fixed delay.
Save to a file path: 'page.png' Use PNG for lossless output; select JPEG or WebP only when supported and appropriate.
Return bytes instead of writing a file const bytes = await page.screenshot({ fullPage: true }) Useful for uploading to storage or processing in memory.

Other screenshot options include clipping to a region, masking locators, hiding the caret, disabling animations, and omitting the background for transparency. A clip captures a specified region rather than serving as a substitute for a complete page. Review the official Page API for option details and version availability.

Lazy-loaded and dynamic content

Some sites load images or sections only after they enter the viewport. A full-page screenshot option does not promise that every such resource has finished loading. For a page you control or are authorized to inspect, scroll through it before capture and wait for a page-specific completion signal. For example, a cautious helper can scroll in increments, allow the page to render, then return to the top:

async function loadByScrolling(page) {
  await page.evaluate(async () => {
    const step = Math.max(300, window.innerHeight);
    for (let y = 0; y < document.body.scrollHeight; y += step) {
      window.scrollTo(0, y);
      await new Promise(resolve => setTimeout(resolve, 150));
    }
    window.scrollTo(0, 0);
  });
  await page.waitForTimeout(300);
}

This is a practical heuristic, not a universal lazy-loading guarantee. Pages that append more content as you scroll may grow during the loop. Prefer a site-specific signal such as a completed-results indicator, and avoid endlessly scrolling pages. For sticky headers, animated content, or components that change while scrolling, the final image may show a state that differs from a static print layout.

6. cURL and HTTP clients: what they can and cannot do

Plain cURL can save an image only when the endpoint already serves an image response. It does not render a web page or execute browser JavaScript, and an ordinary request to a protected page usually receives a login redirect or HTML login form rather than a screenshot. Replaying cookies manually is possible for some sites, but it is brittle, exposes credentials to shell history or logs, and still does not create a browser render.

# This downloads an image response only if the URL itself returns an image.
curl -L 'https://example.com/path/to/image.png' -o image.png

Use Playwright when the target requires a rendered browser session. Use cURL or Python/Node HTTP clients for an image endpoint, or for a screenshot service that is explicitly configured to authenticate to the target. Do not send private page URLs or credentials to a third-party service unless its access model and your organization’s rules allow it.

7. Troubleshooting

Symptom Likely cause Fix
The screenshot shows a login page The state expired, was not loaded, or the site uses authentication data missing from the saved state. Repeat login, confirm the state file path, and check the resulting URL and a protected-page locator before capture.
It works interactively but fails headless The site requires an interactive challenge, different browser setup, or additional session data. Complete the site’s approved login flow, save fresh state, and investigate the site-specific requirement. Do not assume every MFA or bot check can be automated.
Navigation times out The page has long-lived network activity or waits on resources unrelated to the content. Use a suitable navigation condition such as domcontentloaded, then explicitly wait for the page element you need. Increase timeout only when the page legitimately needs more time.
Screenshot is only the visible viewport The full-page option was omitted or set false. Set fullPage: true (Node.js) or full_page=True (Python).
Images or lower sections are blank Lazy loading or client-side rendering had not completed. Scroll to trigger lazy resources, wait for a page-specific ready condition, and capture after content settles.
Browser crashes on a very long page Allocating the full page image can exceed available browser memory. Capture smaller sections or viewport-sized pieces and assemble them if appropriate; reduce viewport/device scale where possible, or capture only the required content. Playwright notes that pages may crash when allocating too much memory.
Screenshot file is missing or empty The script exited before the screenshot completed or wrote to a different working directory. Await the screenshot call, use an explicit output path, and check filesystem permissions.
cURL downloads HTML instead of an image The protected URL returned a login page or normal HTML. Use browser automation for rendered pages, or call a screenshot endpoint that supports the required authentication.
State works once and then stops Session expiry, account revocation, or server-side invalidation. Regenerate the state after signing in. Avoid sharing one account state among concurrent workflows that modify server-side data.

8. Performance, reliability, and cost

A local Playwright capture has no per-screenshot API charge, but it uses compute, browser storage, and engineering time. A full-page image can consume significant memory, especially for pages with large dimensions, many images, or high device scale. A smaller viewport, fewer simultaneous browser contexts, and capturing only the necessary region can reduce resource use. If a page is extremely tall, split it into sections rather than asking the browser to allocate one enormous bitmap.

For repeatable runs, reuse a valid saved state, wait on semantic page conditions, and record the final URL or a failure screenshot when capture prerequisites are not met. Sessions expire and site markup changes, so state refresh and selector maintenance are part of operating the workflow. There is no universal reliability guarantee across MFA, SSO, bot checks, or every private site.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It takes a URL and returns an image or PDF, but a plain URL request does not automatically inherit your local signed-in browser session. Use it for pages the service can access; private, authenticated pages require an access method supported by your setup. 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
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

With a configured capture, ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. 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; paid plans start at $5 for 3,000 screenshots. Start with 1,000 free screenshots a month, no card required.

FAQ

Does full-page mode include content below the fold?

Yes. Playwright’s full-page option captures the full scrollable page rather than just the visible viewport. Dynamic and lazy-loaded content may still need to be triggered and awaited first.

Can I capture a private page from just its URL?

Not reliably. The capture browser needs a valid authenticated session, or the service must have an explicitly supported way to authenticate. A URL alone does not transfer your browser’s login.

Does Playwright storage state include every kind of login state?

It can persist cookies and local storage, and optionally IndexedDB. Session storage needs separate handling, and some sites bind sessions to other browser or server conditions.

Can a screenshot service use my local browser cookies?

Do not assume so. A service needs an explicit supported authentication flow; your local browser state is not automatically shared with it.

Sources