ScreenshotNeo

BlogHow-to

How to Take a Screenshot of a Password-Protected Page with Puppeteer

Use Puppeteer to authenticate before navigation, verify protected content loaded, and capture a reliable screenshot. Covers HTTP auth, web forms, and common failures.

By the ScreenshotNeo team4 October 20267 min read

Direct answer: identify the kind of password gate first. For HTTP authentication, call Puppeteer’s page.authenticate() with credentials before page.goto(). For an HTML login form, complete the site’s authorized sign-in flow or load an authorized session; page.authenticate() does not fill website forms. In either case, verify that expected protected content is present before calling page.screenshot().

This guide is for pages and accounts you are authorized to access. Keep credentials and session cookies private, and avoid capturing secrets that should not be shared.

1. Identify the password gate

What you see Use this approach
A browser-native username and password challenge before the page loads HTTP authentication with page.authenticate(), configured before navigation.
Username and password fields rendered inside a website The site’s normal form or identity-provider flow, followed by a protected-content check.
An existing authorized browser session Use an approved session or browser context; protect any cookies or persisted profile data as credentials.

Puppeteer documents Page.authenticate() for credentials and notes that it enables request interception behind the scenes, which may affect performance. It is not a general-purpose HTML form login method. See the Puppeteer Page API.

2. HTTP authentication: runnable Puppeteer example

Install Puppeteer in a Node.js project with npm install puppeteer. Set the URL and credentials in the environment, then run this as an ES module (for example, save it as capture.mjs and run node capture.mjs).

import puppeteer from 'puppeteer';

const { PAGE_URL, PAGE_USERNAME, PAGE_PASSWORD } = process.env;
if (!PAGE_URL || !PAGE_USERNAME || !PAGE_PASSWORD) {
  throw new Error('Set PAGE_URL, PAGE_USERNAME, and PAGE_PASSWORD');
}

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000 });

  // Configure HTTP credentials before navigating to the protected origin.
  await page.authenticate({ username: PAGE_USERNAME, password: PAGE_PASSWORD });

  const response = await page.goto(PAGE_URL, {
    waitUntil: 'domcontentloaded',
    timeout: 60000,
  });
  if (response && !response.ok()) {
    throw new Error(`Navigation returned HTTP ${response.status()}`);
  }

  // Replace with a stable selector that exists only in the authorized page.
  await page.waitForSelector('[data-page-ready="true"]', {
    visible: true,
    timeout: 30000,
  });
  await page.screenshot({ path: 'protected-page.png', fullPage: true });
} finally {
  await browser.close();
}

Set the environment variables through your shell or secret manager. Do not put real credentials in source control. The readiness selector is intentionally site-specific: replace it with an element that confirms the intended protected content has loaded. A successful navigation alone does not prove authentication succeeded; redirects, expired sessions, and access-denied pages can all load successfully.

The example uses domcontentloaded to avoid assuming every app reaches network idle. Puppeteer’s screenshots guide demonstrates navigation followed by Page.screenshot() and uses networkidle2 as one possible wait strategy. Pick readiness based on the page, then separately verify its authenticated state.

3. Website login forms and authorized sessions

For an HTML form, selectors, redirects, MFA, anti-automation controls, and session duration vary by site. There is no universal username/password selector recipe. Use the application’s authorized login flow and confirm the final protected page contains the expected content. If login is handled by an identity provider, follow that provider’s intended flow and use an approved test account or session.

When clicking a login control triggers navigation, register the navigation wait at the same time as the click so the event is not missed:

const [navigation] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.click('YOUR_LOGIN_SUBMIT_SELECTOR'),
]);

// Navigation is not proof of login. Verify content specific to the target page.
await page.waitForSelector('YOUR_AUTHENTICATED_CONTENT_SELECTOR', {
  visible: true,
  timeout: 30000,
});

Replace both selectors with stable selectors from the site you control or are authorized to access. If the form submits without a full navigation, use an app-specific success condition rather than waiting forever for navigation.

If reusing an authorized session, use the Browser or BrowserContext cookie APIs that match your installed Puppeteer version. Current Page API documentation marks page-level cookie methods deprecated and points to Browser or BrowserContext methods. Treat cookies and persisted browser profiles as authentication secrets: do not log, publish, or expose them in a screenshot. See the waitForSelector API and check the API docs for your installed version.

4. Wait for the right content, then capture

  • Use a positive success condition. Wait for a protected-page heading, unique content container, or app-owned ready marker. A vanished login form can be a secondary check, but by itself may also mean an error or redirect.
  • Check the destination. For redirecting apps, inspect page.url() after login and confirm it is the intended page.
  • Handle rendering separately from authentication. A content selector can appear before charts, images, or client-side data finish rendering. Wait for the specific rendered element or app readiness signal you need in the screenshot.
  • Choose capture scope. fullPage: true captures beyond the viewport; omit it for a viewport screenshot. For a single target, Puppeteer also supports element-level screenshots through ElementHandle.screenshot().
// Full page
await page.screenshot({ path: 'protected-full.png', fullPage: true });

// Or capture a specific element after it is ready
const panel = await page.waitForSelector('[data-report-panel]', { visible: true });
await panel.screenshot({ path: 'protected-panel.png' });

Long-lived connections or pages that continuously poll can make network-idle conditions unsuitable. A site-specific visible selector is often a more direct signal. Neither a network-idle wait nor a completed navigation proves that the expected user is authenticated.

5. Troubleshooting

Symptom Likely cause Fix
Browser credentials prompt remains or access is denied The site may not use HTTP authentication, credentials may be wrong, or authentication was configured after navigation. Confirm the challenge type and host; set page.authenticate() before goto(). For an in-page form, use the site’s login flow.
Screenshot contains the login page Navigation completed but login did not, the session expired, or the app redirected. Check final URL and response where available; wait for a protected-content selector and fail the capture if it never appears.
Screenshot contains an access-denied or challenge page The account lacks access or the site returned an error or anti-automation response. Use an authorized account and the site’s approved access path. Do not treat a loaded page as success; assert the expected content.
Selector wait times out Placeholder selector was not replaced, content differs, or the page has not reached the expected state. Inspect the page and choose a stable selector that belongs to the protected content. Increase the timeout only if the site legitimately needs more time.
Network-idle wait hangs Persistent connections or polling prevent the page from becoming idle. Wait for a page-specific selector or readiness marker instead, with a bounded timeout.
Login click misses navigation The navigation wait began after the click or the form updates in place. Start waitForNavigation() and the click together with Promise.all(); for in-place updates, wait for an app-specific success selector.
Capture is unexpectedly slow page.authenticate() enables request interception internally; page rendering and full-page capture also take time. Use authentication only when needed, wait for the specific required state, and capture only the required area where practical.

6. Reliability, performance, and cost

  • Reliability: make authentication success an explicit condition. Use bounded waits and report a clear error if expected protected content never appears; this prevents silently saving a login or error page.
  • Performance: page.authenticate() turns on request interception behind the scenes and may affect performance, according to Puppeteer’s Page API. Avoid unnecessary waits and oversized full-page captures when a viewport or element is sufficient.
  • Secrets: load credentials from environment variables or a secret store. Keep screenshots, cookies, and browser profiles private if they may reveal account data or tokens.
  • Cost: running Puppeteer yourself means managing the browser runtime and its compute, storage, and maintenance in your own environment. The exact cost depends on where and how often you run captures; no fixed benchmark or cost applies to every setup.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. For pages accessible to the service, make one request and receive an image or PDF; it does not replace a private login flow or grant access to a password-protected page. Use it only where the target can be accessed without credentials, or where a supported authorized public link is appropriate. 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,
)
r.raise_for_status()
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}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));

Cookie and consent banners, newsletter popups, and chat widgets are removed before capture, and each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000. Every feature is on every plan.

Create a free account and get 1,000 screenshots a month with no card.

8. FAQ

Does Puppeteer authenticate to any password-protected page with one method?

No. HTTP authentication and website form authentication are different mechanisms. Choose based on the actual gate.

Does a successful HTTP response mean the screenshot is authenticated?

No. Check for content that is specific to the intended protected page and, when useful, verify the final URL.

Can I reuse a browser login?

Yes, when the session is authorized and handled securely. Use the Browser or BrowserContext cookie APIs for your installed Puppeteer version and protect stored session data.

Should I always wait for network idle?

No. It is one available strategy, but persistent connections can prevent it from completing. Prefer an app-specific readiness condition when appropriate.

Sources