ScreenshotNeo

BlogHow-to

How to Capture a Chromium Screenshot of a Page Behind a Login

Log in with Playwright or Puppeteer, verify the authenticated page, and capture a viewport, element, or full page. Includes state reuse and troubleshooting.

By the ScreenshotNeo team4 October 202611 min read

To capture a Chromium screenshot of a page behind a login, open Chromium with browser automation, sign in through the site’s supported login flow, wait until the authenticated page is ready, and then take the screenshot. The screenshot API does not log in for you: the browser context must already have valid authentication.

This guide uses Playwright with Node.js for a web-form login, then shows how to reuse saved browser state, handle HTTP authentication, capture different page scopes, and troubleshoot common failures. Use an account and page you are authorized to access.

1. Choose the authentication method

First identify what protects the page:

Authentication type Recommended approach
Application login form Automate the normal sign-in UI, then verify a post-login URL or authenticated element.
Existing browser session Save Playwright browser state after an authorized login and load it into a new context while it remains valid.
HTTP Basic or Digest authentication Configure HTTP credentials on the browser context or page; this is different from filling a web form.

Login redirects can take several steps and set cookies along the way. Do not treat a successful button click as proof that login completed. Wait for a stable URL or an element that only appears for an authenticated user. See the [Playwright authentication guide](https://playwright.dev/docs/auth) and [BrowserContext API](https://playwright.dev/docs/api/class-browsercontext).

2. Install Playwright and capture an authenticated page

The following Node.js example launches Chromium, signs in through a form, verifies a post-login URL, visits a protected page, and saves a full-page PNG. Replace the example URLs, selectors, and environment variable names with the target application’s values.

npm install playwright
npx playwright install chromium
// capture-login.mjs
import { chromium } from 'playwright';

const loginUrl = process.env.LOGIN_URL;
const targetUrl = process.env.TARGET_URL;
const username = process.env.LOGIN_USERNAME;
const password = process.env.LOGIN_PASSWORD;

if (!loginUrl || !targetUrl || !username || !password) {
  throw new Error('Set LOGIN_URL, TARGET_URL, LOGIN_USERNAME, and LOGIN_PASSWORD');
}

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

  await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });
  await page.locator('input[name="username"]').fill(username);
  await page.locator('input[name="password"]').fill(password);
  await page.locator('button[type="submit"]').click();

  // Use the condition that matches the application. This example assumes
  // successful sign-in leaves the browser under /app.
  await page.waitForURL('**/app/**', { timeout: 30_000 });
  // An authenticated-only element is a useful additional check when available.
  await page.locator('[data-testid="account-menu"]').waitFor({ timeout: 15_000 });

  await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
  await page.locator('main').waitFor({ state: 'visible', timeout: 30_000 });
  await page.screenshot({ path: 'authenticated-page.png', fullPage: true });

  await context.close();
} finally {
  await browser.close();
}

Run it with credentials supplied by your shell or secret manager rather than hard-coding them:

LOGIN_URL='https://example.com/login' \
TARGET_URL='https://example.com/account/reports' \
LOGIN_USERNAME='your-test-user' \
LOGIN_PASSWORD='your-secret' \
node capture-login.mjs

Selectors are site-specific. Prefer stable labels, roles, or test IDs when the app provides them. If the login form uses email instead of username, update the locator. If successful login goes directly to the target, you can omit the second navigation and verify the final page there.

3. Pick the screenshot scope and readiness condition

Playwright can capture the visible viewport, one element, or the entire scrollable page. Choose based on what the screenshot needs to show:

Scope Example Use it for
Viewport await page.screenshot({ path: 'view.png' }) The page as seen at one browser size.
Element await page.locator('.invoice').screenshot({ path: 'invoice.png' }) A component, chart, receipt, or other specific region.
Full page await page.screenshot({ path: 'full.png', fullPage: true }) Content below the fold as well as the initial viewport.

Wait for the page condition that matters to the capture. A visible main region is a useful baseline, but a dashboard may need a chart, table, or authenticated-only marker to finish rendering. For pages that load data after navigation, wait for that specific content instead of assuming navigation completion means the page is ready. Playwright documents the [screenshot options](https://playwright.dev/mcp/tools/screenshots); Puppeteer documents its [screenshot guide](https://pptr.dev/guides/screenshots).

// Wait for a specific piece of authenticated content.
await page.locator('[data-testid="report-table"]').waitFor({ state: 'visible' });

// Capture just that element.
await page.locator('[data-testid="report-table"]').screenshot({
  path: 'report-table.png',
});

Full-page screenshots can be very tall. If the site loads images or rows only as they enter the viewport, scroll through the page or otherwise trigger the content before capturing, then confirm the result includes it.

4. Save and reuse the authenticated browser state

For repeat captures, sign in once, save the browser context state, and create later contexts from that state. Playwright’s storage state can include cookies, local storage, IndexedDB, and virtual WebAuthn credentials. What is required depends on the target application’s authentication setup. The state file may contain cookies or headers that can impersonate the account, so keep it out of source control and restrict access to it.

Create a local state directory and exclude it from Git:

mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore

After the login verification in the previous script, save state:

await context.storageState({ path: 'playwright/.auth/user.json' });

For a subsequent capture, load that state into a new context and still verify that the session is valid:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
try {
  const context = await browser.newContext({
    storageState: 'playwright/.auth/user.json',
    viewport: { width: 1440, height: 1000 },
  });
  const page = await context.newPage();
  await page.goto('https://example.com/account/reports', {
    waitUntil: 'domcontentloaded',
  });

  // Fail clearly if the saved session expired or redirected to sign-in.
  if (new URL(page.url()).pathname.startsWith('/login')) {
    throw new Error('Saved session is no longer authenticated; regenerate state');
  }
  await page.locator('[data-testid="report-table"]').waitFor({ state: 'visible' });
  await page.screenshot({ path: 'report.png', fullPage: true });
  await context.close();
} finally {
  await browser.close();
}

Regenerate the state when it expires or no longer authenticates. Playwright’s standard storage-state mechanism does not automatically persist sessionStorage. If the application relies on it, follow the [documented session-storage workaround](https://playwright.dev/docs/auth#session-storage) to save it and install it before the app code runs. Avoid that extra mechanism when cookies or local storage are sufficient.

For parallel tests that change server-side data, use separate test accounts when needed. Sharing one account can make concurrent runs interfere with one another. See Playwright’s [authentication guidance](https://playwright.dev/docs/auth) for its account and state-file cautions.

5. Handle HTTP authentication separately

When Chromium shows a browser-level HTTP authentication challenge, configure credentials on the browser context. Do not fill a username and password into an application form unless the site actually uses one.

import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const context = await browser.newContext({
    httpCredentials: {
      username: process.env.HTTP_USERNAME,
      password: process.env.HTTP_PASSWORD,
    },
  });
  const page = await context.newPage();
  await page.goto('https://protected.example.com/report', {
    waitUntil: 'domcontentloaded',
  });
  await page.locator('main').waitFor({ state: 'visible' });
  await page.screenshot({ path: 'http-auth-page.png', fullPage: true });
  await context.close();
} finally {
  await browser.close();
}

Puppeteer supports HTTP authentication with page.authenticate({ username, password }). Its documentation notes that this feature enables request interception behind the scenes, which can affect performance. Consult the [Puppeteer Page API](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.page.md?plain=1) for the method details.

6. Python and command-line alternatives

Python Playwright follows the same browser workflow. Install the package and Chromium, then run this script with the same environment variables as the Node.js example:

pip install playwright
python -m playwright install chromium
# capture_login.py
import os
from playwright.sync_api import sync_playwright

login_url = os.environ['LOGIN_URL']
target_url = os.environ['TARGET_URL']
username = os.environ['LOGIN_USERNAME']
password = os.environ['LOGIN_PASSWORD']

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    try:
        context = browser.new_context(
            viewport={"width": 1440, "height": 1000},
            device_scale_factor=1,
        )
        page = context.new_page()
        page.goto(login_url, wait_until='domcontentloaded')
        page.locator('input[name="username"]').fill(username)
        page.locator('input[name="password"]').fill(password)
        page.locator('button[type="submit"]').click()
        page.wait_for_url('**/app/**', timeout=30_000)
        page.locator('[data-testid="account-menu"]').wait_for(timeout=15_000)
        page.goto(target_url, wait_until='domcontentloaded')
        page.locator('main').wait_for(state='visible', timeout=30_000)
        page.screenshot(path='authenticated-page.png', full_page=True)
        context.close()
    finally:
        browser.close()

Run it with LOGIN_URL, TARGET_URL, LOGIN_USERNAME, and LOGIN_PASSWORD set in the environment. For a repeat workflow, Python can also save and load Playwright storage state with context.storage_state(path='playwright/.auth/user.json') and browser.new_context(storage_state='playwright/.auth/user.json').

cURL can request a page protected by HTTP Basic authentication, but it does not run Chromium or produce a browser screenshot. It also does not complete an application login form. For HTTP Basic, a diagnostic request looks like:

curl --fail --user "$HTTP_USERNAME:$HTTP_PASSWORD" \
  'https://protected.example.com/report' \
  -o response.html

Use the browser workflow above when the deliverable must be a Chromium-rendered screenshot. Avoid placing credentials directly in shell history or logs.

7. Make captures repeatable and protect the output

  • Keep the rendering environment consistent. Pin or record the Playwright/runtime and Chromium versions, viewport, device scale factor, headless mode, and relevant browser settings. Operating system, browser version, hardware, power source, and headless mode can all affect rendering; see [Playwright visual comparisons](https://playwright.dev/docs/test-snapshots).
  • Use a deliberate readiness check. Wait for the authenticated state and the content being captured. A navigation event alone may precede application data rendering.
  • Keep secrets private. Treat credentials, cookies, storage-state files, and screenshots containing private information as sensitive. Store them with restricted access and keep state files out of repositories.
  • Refresh expired state. If the app redirects to login or an authenticated marker is absent, perform a fresh authorized login and save new state.
  • Use test accounts responsibly. Avoid parallel state-changing runs against one shared account when they could affect one another.
  • Keep screenshots scoped. Capture only the page or region needed, and check for private data before sharing the resulting image.

Browser screenshots have no fixed cost per capture in the automation APIs shown here, but your runtime, CI workers, and browser infrastructure do consume resources. Reusing a valid state avoids repeating the UI login, though it adds the operational responsibility of protecting and refreshing that state.

8. Troubleshooting

Symptom Likely cause Fix
Screenshot shows the login page Login did not complete, the session expired, or the target navigation lost the authenticated context. Wait for a post-login URL or authenticated-only element; inspect redirects; regenerate expired state; navigate with the same context.
Login wait times out The expected URL or selector does not match the app, the identity flow takes longer, or login failed. Check the actual landing URL and page state; use a stable authenticated marker; allow for the site’s redirect flow and inspect its visible error message.
Credentials appear correct but authentication fails The page may use a different field selector, MFA, an identity provider, or an application-specific flow. Use the application’s supported sign-in process, update selectors, and complete required authorized authentication steps. Do not assume every site can be automated identically.
Saved state works locally but fails later Cookies expired, state was revoked, or required session storage was not included. Log in again and save fresh state. If the app uses session storage, add the documented explicit handling.
Screenshot is blank or content is missing The capture ran before app data or lazy content rendered, or the selected element was wrong. Wait for the actual content selector; scroll to trigger lazy content; confirm the element is visible before capture.
Image differs between runs Browser/runtime, viewport, scale, OS, headless mode, or page data changed. Pin and record the browser environment, use a fixed viewport and scale, and stabilize test data where possible.
HTTP auth keeps prompting or returns an error The server uses different credentials or an application login rather than HTTP auth. Confirm the protection type and credential scope; use httpCredentials only for HTTP authentication.
State file appears in Git changes The auth directory was not excluded, or a state file was already tracked. Add the directory to .gitignore and remove any tracked credential state from version control according to your repository’s secret-handling process.

9. Or skip the browser setup

If the target page is publicly reachable, ScreenshotNeo can return an image or PDF from one API request. It does not use your browser’s private login session, so this is not a way to capture a page that requires your account’s web-form authentication. ScreenshotNeo accepts the cookie or consent banner like a visitor 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, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.

See the [ScreenshotNeo documentation](https://screenshotneo.com/docs/) for request options and setup.

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 is a website screenshot API and MCP server by [Yorker Media](https://screenshotneo.com). It includes 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 screenshots. [Create a free account](https://screenshotneo.com/account/sign-up/) to make up to 1,000 screenshots a month at no charge and without a card.

FAQ

Can I screenshot a page that requires MFA?

That depends on the site’s supported sign-in flow and your authorized test setup. Complete the required authentication, then verify the signed-in page before saving state or capturing.

Does Playwright storage state include every browser storage mechanism?

No. Its standard storage-state flow does not automatically include session storage. Use explicit handling only if the application relies on it.

Can a screenshot prove that a user was authenticated?

No. It records rendered pixels, not a trustworthy audit trail of how the page was accessed. Use application logs or other appropriate records when you need evidence of authentication.

Why does the screenshot contain personal or account data?

The browser captured the rendered page as requested. Review the page and output before sharing, and use a test account or sanitized data when appropriate.