ScreenshotNeo

BlogHow-to

How to Screenshot a Webpage After Logging In with Puppeteer

Log in with Puppeteer, confirm the authenticated state, and capture a page with reliable waits. Includes runnable JavaScript, troubleshooting, and an API alternative.

By the ScreenshotNeo team4 October 20269 min read

To screenshot a page after logging in with Puppeteer, complete the website’s normal login flow in a browser page, wait for a reliable signal that login succeeded, navigate to the target page in the same browser context, wait for its content, and call page.screenshot(). Use fullPage: true to capture the full scrollable page. For ordinary HTML login forms, interact with the form; page.authenticate() is for HTTP authentication.

The selectors below are examples. Replace them with selectors and success conditions from the site you are authorized to automate. Login flows, redirects, MFA, and session persistence vary by site.

1. Install Puppeteer

This example uses JavaScript modules and the current Puppeteer API. In a new project, install Puppeteer and save the script as screenshot-after-login.mjs:

npm install puppeteer

Set credentials through environment variables rather than committing them to source control:

export SITE_USERNAME='your-username'
export SITE_PASSWORD='your-password'
node screenshot-after-login.mjs

2. Log in, verify the session, and capture

This runnable template assumes a form login that causes a document navigation and a target page with identifiable content. Change the URLs and selectors to match your site.

import puppeteer from 'puppeteer';

const username = process.env.SITE_USERNAME;
const password = process.env.SITE_PASSWORD;
if (!username || !password) {
  throw new Error('Set SITE_USERNAME and SITE_PASSWORD before running this script.');
}

const browser = await puppeteer.launch({ headless: true });
try {
  // A new page belongs to the browser's default context. Keep login and
  // capture in this context so the authenticated session remains available.
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000 });

  await page.goto('https://example.com/login', {
    waitUntil: 'domcontentloaded',
  });
  await page.locator('input[name="username"]').fill(username);
  await page.locator('input[name="password"]').fill(password);

  // Use this pattern when submitting the form causes a document navigation.
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.locator('button[type="submit"]').click(),
  ]);

  // Replace with a site-specific proof of successful authentication.
  await page.locator('[data-testid="account-home"]').wait();

  await page.goto('https://example.com/account/report', {
    waitUntil: 'domcontentloaded',
  });
  // Wait for the content you actually need in the image.
  await page.locator('[data-testid="report-content"]').wait();

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

Puppeteer’s locators wait for elements to be present and in the expected state. A post-login account element, known URL, or target content can serve as a success signal. Choose a condition that is meaningful for the application; reaching a URL alone may not mean data has finished rendering. See the official Puppeteer page interactions guide and Page API.

3. Handle login flows that do not navigate

Some applications submit login requests in the background and update the current page without a full navigation. In that case, do not wait for navigation. Click the submit control, then wait for the application’s authenticated state:

await page.locator('button[type="submit"]').click();
await page.locator('[data-testid="account-home"]').wait();

If the application redirects through several pages, wait for a specific destination URL or stable authenticated element. If an MFA challenge is required, the automation must follow the site’s permitted MFA flow; the generic example cannot provide universal MFA selectors or bypass a challenge.

4. Choose the right authentication method

Authentication type What to do Important detail
HTML login form Navigate to the login page, fill its fields, submit, and wait for a success condition. This is the usual approach for a website login page. Selectors and redirects are site-specific.
HTTP authentication Call page.authenticate({ username, password }) before navigating. This handles an HTTP authentication challenge, not a regular form. Puppeteer documents that it enables request interception behind the scenes, which can affect performance.
Existing cookies Set the cookies in the browser context before opening the target, if the application’s session mechanism supports it. Cookies may not be the only required state; some apps also rely on local storage or other browser state.

For HTTP authentication, configure credentials before the protected navigation:

await page.authenticate({
  username: process.env.HTTP_USERNAME,
  password: process.env.HTTP_PASSWORD,
});
await page.goto('https://protected.example.com/report');

The Puppeteer method is explicitly for HTTP authentication. Avoid enabling request interception unless you need it. When interception is on, every request must be continued, responded to, or aborted by a handler; unresolved requests can stall page loading. Details are in Puppeteer’s request interception guide.

5. Keep the authenticated session in the right browser context

Cookies and local storage are scoped to a browser context. Log in and capture in the same context, or deliberately transfer the state the application requires. A separate context is isolated from the first one and will not automatically have its session. See the BrowserContext API and Puppeteer’s cookie guide.

For a one-off capture, using the same page is simplest. If you open another page in the same context, it can use that context’s session cookies, but still verify that the application recognizes the session. For repeated jobs, do not assume session state survives a new browser launch unless you have intentionally configured and secured persistent storage.

6. Wait for the actual page content

Choose waits based on what the screenshot must contain:

  • Application selector: wait for the report, table, or other specific element that should appear in the image.
  • Navigation: if a click triggers a document navigation, start waitForNavigation() before clicking. Puppeteer’s documented pattern uses Promise.all() to avoid a race.
  • Network idle: page.waitForNetworkIdle() can be useful when a page becomes quiet after loading, but network idle by itself does not prove that the required application content is ready. Some applications keep connections open or load content after the network quiets.
  • Short delay: use a delay only when the site has a known timing need and no better readiness signal. A fixed delay can be too short on a slow run and waste time on a fast one.

For example, add a selector wait after navigation, and optionally follow it with network idle when that fits the page:

await page.goto('https://example.com/account/report', {
  waitUntil: 'domcontentloaded',
});
await page.locator('[data-testid="report-content"]').wait();
await page.waitForNetworkIdle({ idleTime: 500, timeout: 10000 }).catch(() => {
  // Keep the selector as the primary readiness signal when network idle
  // is unsuitable for this application.
});

See the official waitForNetworkIdle API for its timing behavior.

7. Configure the screenshot

The default screenshot captures the viewport. Set fullPage: true to capture the full page. Puppeteer’s screenshot options also include output path, image type, quality, clipping, and background handling. Consult the ScreenshotOptions reference for the current option details.

// Viewport screenshot (the default coverage)
await page.screenshot({ path: 'viewport.png' });

// Full scrollable page
await page.screenshot({ path: 'full-page.png', fullPage: true });

// JPEG output with quality; quality applies to JPEG screenshots
await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 85 });

// Capture a specific rectangle in page coordinates
await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 0, width: 900, height: 600 },
});

// Omit the default page background where supported by the output
await page.screenshot({ path: 'transparent.png', omitBackground: true });

Use a viewport screenshot for a specific visible state, such as a dashboard above the fold. Use a full-page screenshot for a long report or article. A clipped capture is useful for a known region, but the coordinates must match the rendered page. Full-page images can become very tall and consume more memory than viewport captures.

8. Troubleshooting

Symptom Likely cause Fix
Screenshot shows the login form Credentials were rejected, the submit did not occur, or the script moved on before login finished. Check that the fields and submit selector match the site. Wait for a post-login element or URL before navigating to the target.
waitForNavigation() times out The site uses an in-page request instead of a document navigation. Remove the navigation wait and wait for a success element or application state after clicking.
Script hangs after clicking submit The click does not cause navigation, or a navigation is waiting on a condition the page never reaches. Use the no-navigation pattern, or select a navigation wait condition suited to the site; keep a success selector as the decisive check.
Target page redirects to login The target is opened in a different context, session state expired, or the login did not complete. Use the same context, confirm the authenticated state, and inspect the target URL after navigation.
Element selector wait times out The selector is wrong, the content is conditional, or the page failed to load. Inspect the page structure and use a stable site-specific selector. Check for error pages and redirects before waiting indefinitely.
Screenshot is blank or incomplete The capture happened before the app rendered, or content is lazy-loaded. Wait for the required content and, where needed, scroll or trigger the site’s lazy-loading behavior before capture.
HTTP credentials do not log in through the form page.authenticate() was used for an ordinary HTML form. Fill and submit the form. Reserve page.authenticate() for HTTP authentication challenges.
Requests stall after enabling interception A request handler did not resolve every intercepted request. Ensure each handler continues, responds to, or aborts requests, or disable interception if it is not needed.
Full-page capture is unexpectedly large or slow The page is very tall or contains many rendered resources. Capture the viewport or a relevant clip when full-page coverage is unnecessary; avoid rendering content the image does not need.

9. Performance, reliability, and cost

  • Wait on meaningful state: a specific content selector usually makes readiness easier to reason about than a long fixed sleep. Network idle can supplement that signal, but is not a substitute for it.
  • Keep browser work bounded: close the browser in a finally block so failures do not leave the process running. Set suitable timeouts for your job and handle navigation or selector failures explicitly.
  • Reuse a browser carefully: reusing a browser process can avoid repeated startup work in a batch, but keep user sessions isolated by browser context and close pages and contexts when finished.
  • Protect credentials and session state: use environment secrets or a secret manager, avoid logging passwords and session cookies, and follow the website’s permitted automation practices. Treat saved authentication state as sensitive.
  • Control image size: viewport captures are smaller than tall full-page captures. Choose output format and dimensions to fit downstream storage and display needs.
  • Cost: self-hosted Puppeteer does not have a per-screenshot ScreenshotNeo API charge, but your runtime, browser infrastructure, maintenance, storage, and failure handling still have costs. A managed API trades browser setup for per-plan usage.

Or skip the browser setup

If you do not want to run and maintain a browser for each capture, ScreenshotNeo takes a screenshot from one GET request. Its API documentation covers the options and response headers.

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}`);
await Bun.write('shot.webp', res);

These examples capture a publicly reachable URL. A Puppeteer login session is browser state; do not assume a screenshot API can reuse it. ScreenshotNeo’s documented features include custom headers, cookies, and Authorization for requests where those credentials are appropriate. Cookie banners, popups, and chat widgets are removed 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, and paid plans start at $5 for 3,000. See the docs, then sign up free for 1,000 screenshots a month, with no card.

FAQ

Can Puppeteer capture a page that requires login?

Yes. Complete the site’s login flow and capture the target in a browser context that has the required session state. The precise flow depends on the site.

Does fullPage: true capture content below the fold?

It requests a full-page capture rather than the default viewport image. Pages with lazy-loaded content may need additional scrolling or application-specific waits before capture.

Can I use this for a page behind MFA?

Only if your automation follows the site’s permitted authentication process and can complete the required challenge. The generic template does not implement a universal MFA flow.

Why not use network idle as the only wait?

Network quiet does not establish that the particular logged-in content you need is present. Wait for a meaningful application element as well.