ScreenshotNeo

BlogHow-to

How to capture a screenshot of a page behind a login with Puppeteer

Log in with Puppeteer, verify the protected page actually loaded, then capture it. Includes form login, authorized cookies, HTTP authentication, and troubleshooting.

By the ScreenshotNeo team4 October 202610 min read

To capture a page behind a login with Puppeteer, establish an authorized browser session, navigate to the protected URL, wait for a page-specific sign of successful authentication, and then call page.screenshot(). For a full-page image, set fullPage: true. Logging in is site-specific, so verify the final URL and a unique element on the protected page before saving the screenshot.

This guide covers the usual web-app login form, restoring a valid session with cookies, HTTP authentication, full-page and element captures, and common failures. Use only accounts and session credentials you are authorized to access.

1. Install Puppeteer and prepare the capture

In a new Node.js project, install Puppeteer:

npm install puppeteer

Puppeteer normally downloads a compatible browser during installation. If your environment supplies its own Chrome or Chromium, configure the executable path in puppeteer.launch() and ensure the browser version is compatible with the installed Puppeteer package.

Save the following as screenshot-private.mjs. Replace the URLs, form selectors, credentials, and protected-page marker with values for your own site. The example submits a standard HTML login form and then checks that the destination is genuinely authenticated before taking a full-page PNG.

import puppeteer from 'puppeteer';

const LOGIN_URL = 'https://example.com/login';
const PRIVATE_URL = 'https://example.com/account/private';
const USERNAME = process.env.SITE_USERNAME;
const PASSWORD = process.env.SITE_PASSWORD;

if (!USERNAME || !PASSWORD) {
  throw new Error('Set SITE_USERNAME and SITE_PASSWORD in the environment.');
}

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

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

  // Replace this with a reliable marker shown only after successful login.
  await page.waitForSelector('[data-testid="account-home"]', {
    timeout: 15000,
  });

  const response = await page.goto(PRIVATE_URL, {
    waitUntil: 'domcontentloaded',
  });
  if (response && response.status() >= 400) {
    throw new Error(`Protected page returned HTTP ${response.status()}`);
  }

  // This must be specific to the authenticated destination, not a shared nav item.
  await page.waitForSelector('[data-page="private-account"]', {
    timeout: 15000,
  });
  console.log('Captured URL:', page.url());
  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await browser.close();
}

Run it with credentials supplied through environment variables:

SITE_USERNAME='your-user' SITE_PASSWORD='your-password' node screenshot-private.mjs

Do not commit credentials or print passwords, cookies, or authorization headers to logs. In deployment, use your platform’s secret store. This sample is an adaptable pattern, not a universal login recipe: a site’s form fields, redirects, MFA, consent steps, and success marker vary.

2. Choose how Puppeteer gets an authenticated session

There are three common cases. Use the site’s ordinary login flow for an application login, a legitimately obtained unexpired cookie session when you need to restore a session, and page.authenticate() only when the server requests HTTP authentication.

Approach Use it when Things to account for
Automate the site’s login form The site uses a normal web-app login page. Selectors, MFA, redirects, consent prompts, and success markers are site-specific.
Restore authorized cookies You already have valid session cookies and need to reuse that authorized session. Cookies expire and may be scoped to a domain, path, and security context. Treat them like passwords.
page.authenticate() The server challenges the browser for HTTP authentication. It is not a substitute for filling an HTML login form; it enables request interception and may affect performance.

Automate the site’s normal login form

Drive the same authorized flow a user would use. Wait for the login controls, fill them, submit, and wait for a success condition. Then navigate to the protected URL if the login flow did not already take you there. A unique account or page marker is more reliable than assuming that a click or redirect means the session is valid.

For sites with MFA or interactive bot controls, do not assume a generic script can complete the flow. Use a supported test account or an authorized session method provided for your environment. A wait for navigation alone can finish on a login page after a failed sign-in.

Restore a valid session with cookies

If you have a valid session obtained legitimately, set its cookies on the browser context before visiting the protected page. The current Puppeteer API direction is to use browser or browser-context cookie APIs; the page-level setCookie method is deprecated.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const context = await browser.createBrowserContext();
  await context.setCookie({
    name: 'session',
    value: process.env.SITE_SESSION_COOKIE,
    domain: 'example.com',
    path: '/',
    secure: true,
    httpOnly: true,
    sameSite: 'Lax',
  });

  const page = await context.newPage();
  const response = await page.goto('https://example.com/account/private', {
    waitUntil: 'domcontentloaded',
  });
  if (response && response.status() >= 400) {
    throw new Error(`Destination returned HTTP ${response.status()}`);
  }
  await page.waitForSelector('[data-page="private-account"]');
  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await browser.close();
}

Cookie names, values, and attributes above are examples; use the exact cookie data and scope required by your application. Some sites require multiple cookies or additional browser storage. Never expose session cookies: anyone who obtains a usable session cookie may be able to access the account until it expires or is revoked.

Handle HTTP authentication

For a server-level HTTP authentication challenge, authenticate before navigation:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.authenticate({
    username: process.env.HTTP_USERNAME,
    password: process.env.HTTP_PASSWORD,
  });
  const response = await page.goto('https://example.com/private', {
    waitUntil: 'domcontentloaded',
  });
  if (response && response.status() >= 400) {
    throw new Error(`Page returned HTTP ${response.status()}`);
  }
  await page.waitForSelector('[data-page="private-content"]');
  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await browser.close();
}

Puppeteer documents page.authenticate() for HTTP authentication. It turns on request interception behind the scenes, which may affect performance. Passing null disables authentication. It does not fill in an application’s HTML form.

3. Verify the destination before capturing

A completed navigation is not proof that the page is the intended authenticated content. A protected URL may redirect back to login, show an access-denied screen, or return an HTTP error. Check several signals that fit your site:

  1. Inspect page.url() after navigation to detect a redirect to the login route.
  2. Check the main navigation response status where available. In headless shell, valid HTTP error responses such as 404 and 500 do not necessarily make page.goto() throw.
  3. Wait for a unique element or application state present on the protected page. Avoid generic selectors such as a site header that also appears on the login page.
  4. If the page is a single-page application, wait for the relevant content or a known loading indicator to disappear, rather than relying only on the initial document response.
const response = await page.goto('https://example.com/account/private', {
  waitUntil: 'domcontentloaded',
});

if (page.url().includes('/login')) {
  throw new Error('Redirected to login; the session is not authenticated.');
}
if (response && response.status() >= 400) {
  throw new Error(`HTTP ${response.status()} at destination`);
}
await page.waitForSelector('[data-page="private-account"]', {
  visible: true,
  timeout: 15000,
});

Choose a readiness marker that appears only after the protected content is ready. Screenshot operations coordinate with some browser operations, but do not guarantee that your application has finished loading data or rendering every component.

4. Capture a full page, region, or element

Once the correct page is ready, use Page.screenshot(). The default is a viewport screenshot; set fullPage: true to capture beyond the viewport.

await page.screenshot({ path: 'page.png', fullPage: true });

Relevant screenshot options include:

Option What it controls
path Output file location. If omitted, screenshot data is returned instead of being saved to a path. The extension can determine the image type.
fullPage Whether to capture the full document beyond the viewport. Defaults to false.
type Image format where supported. PNG is the default unless the output path indicates another supported type.
quality Compression quality for formats that support it, such as JPEG. It does not apply to PNG.
clip A rectangular region of the page to capture.
omitBackground Whether to omit the default background when the selected format supports transparency.
captureBeyondViewport Controls capture beyond the viewport in applicable screenshot configurations; for a straightforward full-page capture, use fullPage.

For an element-only image, select the element and call ElementHandle.screenshot(). Puppeteer scrolls the element into view when needed. The element must remain attached to the DOM through the capture.

const panel = await page.waitForSelector('[data-testid="billing-panel"]');
if (!panel) throw new Error('Billing panel was not found');
await panel.screenshot({ path: 'billing-panel.png' });

For a PDF rather than an image, Puppeteer provides page.pdf(); page layout and print styles affect the output, so it is a separate capture path from page.screenshot().

5. Troubleshoot common failures

Symptom Likely cause Fix
The screenshot shows the login page. Login failed, the session was not retained, or the destination redirected to login. Check page.url(), wait for a login success marker after submitting, and verify a destination-specific selector before capture.
waitForSelector times out. The selector is incorrect, the element is not visible yet, login failed, or the page’s content loads asynchronously. Confirm the selector in the authorized browser session, inspect the final URL, and wait for the application’s actual readiness condition.
The destination returns 404 or 500 but the script continues. A valid HTTP error response can resolve from navigation without throwing, including in headless shell. Check response.status() and stop before taking a misleading screenshot.
The image is blank or shows a loading state. The screenshot ran before client-side data or visual content was ready. Wait for a unique content element, an application-ready state, or a known loading indicator to disappear.
Cookie restore still redirects to login. The cookie expired, has the wrong domain/path/security scope, or the app needs additional session state. Obtain a fresh authorized session, use the correct cookie attributes, and verify the protected-page marker before capture.
page.authenticate() does not sign in to the app. The target uses a normal HTML login form rather than HTTP authentication. Automate the site’s form or restore an authorized application session instead.
Element screenshot throws because the element detached. The page rerendered and removed the selected node before capture. Re-query the element immediately before capturing and ensure the page has settled.
The full-page image is unexpectedly large or slow. The page is long, has large images, or renders extensive content. Capture only the needed element or region, reduce the viewport/device scale where acceptable, or save a compressed supported format.

6. Performance, reliability, and cost

Browser startup and page rendering are part of the capture cost in time and compute. Reusing a browser process can reduce repeated startup work in a controlled service, but isolate users and credentials in separate browser contexts and close pages and browsers when jobs finish. HTTP authentication enables interception, which may affect performance.

For reliable captures, set explicit timeouts, wait for an application-level ready condition, inspect navigation status, and close the browser in a finally block. Consider a bounded retry only for transient navigation or service failures; do not repeatedly retry invalid credentials, expired sessions, or a missing selector without changing the cause. Avoid logging sensitive session data.

There is no universal runtime or cost figure for this flow: it depends on the site, page length, browser environment, network, and capture volume. For an existing Node.js service, Puppeteer means you operate the browser runtime and handle authentication/session lifecycle. A screenshot API can avoid that browser setup for pages it can access publicly, but an API request cannot magically authenticate to a private account unless the service supports an authorized authentication method.

Or skip the browser setup

If the page is publicly reachable, ScreenshotNeo can capture it with one GET request. See the API documentation for parameters and response behavior. For example, with cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

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)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Yearly billing gives two months free, and every feature is on every plan.

Private pages: the request above captures a publicly accessible URL; do not send private account credentials or session cookies unless you have confirmed the service and its documented authentication options support your use case. For pages that require your logged-in browser session, use the Puppeteer methods above.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Can Puppeteer take a screenshot after a redirect?

Yes. After the redirect, inspect the final URL and wait for a unique marker on the protected destination before capturing. A redirect by itself does not establish that login succeeded.

Should I use cookies or automate the login form?

Use the normal form when that is the supported way to sign in. Restore cookies when you already have a valid, authorized session and need to reuse it; cookie validity and scope are site-specific.

Does fullPage: true capture content that loads only after scrolling?

It captures the document beyond the viewport, but applications that lazy-load content may need scrolling or another site-specific readiness step before capture.

Can I use page.authenticate() for a username and password form?

No. It handles HTTP authentication challenges. For a web-app login form, interact with the page’s controls or use a valid authorized session.