ScreenshotNeo

BlogHow-to

How to Take a Screenshot of a Website Behind a Login with Puppeteer

Log in with Puppeteer, confirm the authenticated page is ready, and save a viewport, full-page, or element screenshot. Includes cookie and HTTP authentication workflows.

By the ScreenshotNeo team4 October 202610 min read

To screenshot a page behind a login with Puppeteer, authenticate in the way that site expects, verify a site-specific signed-in state, navigate to the protected page if needed, and then call page.screenshot(). A normal HTML login form, an existing session cookie, and HTTP authentication are separate workflows. There is no universal login selector or success marker.

Use these steps only for accounts and pages you are authorized to access. Keep passwords and session cookies out of source code, screenshots, logs, and public repositories.

1. Set up Puppeteer

The examples use Puppeteer’s current API style. The official documentation reviewed for this guide is version 25.12.0; check the API reference against the Puppeteer version installed in your project.

npm install puppeteer

Save the following as screenshot-login.js. Set the environment variables and replace the example URLs and selectors with values from the site you control or are authorized to use.

const puppeteer = require('puppeteer');

async function main() {
  const loginUrl = process.env.LOGIN_URL;
  const protectedUrl = process.env.PROTECTED_URL;
  const username = process.env.SITE_USERNAME;
  const password = process.env.SITE_PASSWORD;

  // Replace these selectors with the site's actual form and signed-in marker.
  const usernameSelector = process.env.USERNAME_SELECTOR;
  const passwordSelector = process.env.PASSWORD_SELECTOR;
  const submitSelector = process.env.SUBMIT_SELECTOR;
  const signedInSelector = process.env.SIGNED_IN_SELECTOR;

  for (const [name, value] of Object.entries({
    loginUrl, protectedUrl, username, password,
    usernameSelector, passwordSelector, submitSelector, signedInSelector,
  })) {
    if (!value) throw new Error(`Missing required environment variable: ${name}`);
  }

  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
    page.setDefaultTimeout(15000);
    await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });

    await page.locator(usernameSelector).fill(username);
    await page.locator(passwordSelector).fill(password);

    // Register the navigation wait before clicking to avoid a race.
    await Promise.all([
      page.waitForNavigation({ waitUntil: 'domcontentloaded' }).catch(() => null),
      page.locator(submitSelector).click(),
    ]);

    // This site-specific check also works as an SPA completion signal.
    await page.waitForSelector(signedInSelector, { visible: true, timeout: 20000 });

    // Open the target page if login did not already redirect there.
    if (new URL(page.url()).pathname !== new URL(protectedUrl).pathname) {
      await page.goto(protectedUrl, { waitUntil: 'domcontentloaded' });
    }

    // Confirm the protected content is present before capturing it.
    await page.waitForSelector(signedInSelector, { visible: true, timeout: 20000 });
    await page.screenshot({ path: 'protected-page.png' });
    console.log(`Saved screenshot from ${page.url()}`);
  } finally {
    await browser.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

Example invocation (replace the selectors with the target site’s real selectors):

LOGIN_URL='https://example.com/sign-in' \
PROTECTED_URL='https://example.com/account' \
SITE_USERNAME='your-user' SITE_PASSWORD='your-secret' \
USERNAME_SELECTOR='input[name="email"]' \
PASSWORD_SELECTOR='input[name="password"]' \
SUBMIT_SELECTOR='button[type="submit"]' \
SIGNED_IN_SELECTOR='[data-testid="account-menu"]' \
node screenshot-login.js

The selectors above are illustrative, not universal. Inspect the page you are authorized to automate and choose its actual username, password, submit, and signed-in elements. A multifactor authentication step, CAPTCHA, identity-provider redirect, or account challenge may require an approved site-specific flow; do not assume a successful click means login succeeded.

2. Choose the authentication method

Standard HTML login form

Use the site’s actual form fields and submit action, as in the runnable example. Page.authenticate() does not fill a normal website form; it is for HTTP authentication. Login may navigate to another document or update a single-page application in place, so wait for the navigation when applicable and also verify a meaningful authenticated marker.

If you have a valid session cookie for an authorized account, set it in the browser context before navigating to the protected page. Cookie values are credentials: source them from secure configuration and do not print or publish them. The cookie’s domain, path, expiry, and security attributes must match the target site. A cookie cannot revive an expired or revoked session.

const puppeteer = require('puppeteer');

async function main() {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const context = browser.defaultBrowserContext();
    await context.setCookie({
      name: process.env.SESSION_COOKIE_NAME,
      value: process.env.SESSION_COOKIE_VALUE,
      domain: 'example.com', // Set the actual cookie domain.
      path: '/',
      secure: true,
      httpOnly: true,
      sameSite: 'Lax',
    });
    const page = await context.newPage();
    await page.goto('https://example.com/account', { waitUntil: 'domcontentloaded' });
    await page.waitForSelector('[data-testid="account-menu"]', { visible: true });
    await page.screenshot({ path: 'account.png', fullPage: true });
  } finally {
    await browser.close();
  }
}

main().catch(error => { console.error(error); process.exitCode = 1; });

Replace the cookie name, value, domain, and attributes with those appropriate to the site. Current Puppeteer cookie management uses Browser.setCookie() or BrowserContext.setCookie(); page-level cookie methods are deprecated. See the Puppeteer cookie guide, BrowserContext.setCookie() reference, and CookieData reference.

HTTP authentication

For a server that presents an HTTP authentication challenge, call page.authenticate() before navigation. This is different from submitting a website’s login form. Puppeteer notes that this method enables request interception behind the scenes, which can affect performance.

const puppeteer = require('puppeteer');

async function main() {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.authenticate({
      username: process.env.HTTP_AUTH_USERNAME,
      password: process.env.HTTP_AUTH_PASSWORD,
    });
    await page.goto('https://example.com/protected', { waitUntil: 'domcontentloaded' });
    await page.waitForSelector('main', { visible: true });
    await page.screenshot({ path: 'http-auth-page.png' });
  } finally {
    await browser.close();
  }
}

main().catch(error => { console.error(error); process.exitCode = 1; });

Use the protected site’s real content marker instead of the generic main example. See the Puppeteer Page.authenticate() reference.

3. Wait for authentication and page readiness

A completed document navigation does not prove that authentication succeeded or that protected content has rendered. Wait for a site-specific signal such as an account control, protected heading, or known application state. If the login action triggers a navigation, register the wait before the action:

await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.locator('button[type="submit"]').click(),
]);
await page.waitForSelector('[data-testid="signed-in-marker"]', { visible: true });

For an SPA that does not cause a document navigation, waiting for the authenticated marker can be the primary completion check. Puppeteer treats History API URL changes as navigation, but checking the expected page content still catches cases where the app changed its URL without rendering the right state. A fixed sleep can be useful for a known animation or delayed widget, but it is not evidence of authentication.

Choose a readiness condition that matches the screenshot. For example, wait for the report title or chart container after the signed-in marker appears. If an image or chart loads asynchronously, wait for its element or another explicit completion signal before capturing.

4. Capture the viewport, full page, or one element

Viewport screenshot

page.screenshot() captures the current viewport by default. Set a viewport before navigation if the layout depends on screen size.

await page.setViewport({ width: 1440, height: 1000 });
await page.screenshot({ path: 'viewport.png' });

Full-page screenshot

Set fullPage: true to capture the full document rather than just the viewport.

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

Element screenshot

When only a component is needed, wait for it and capture its element. Puppeteer scrolls an off-screen element into view for the capture.

const report = await page.waitForSelector('[data-testid="report"]', { visible: true });
await report.screenshot({ path: 'report.png' });

Format, quality, and clipping

PNG is the default format. Screenshot options also support image type and a clip rectangle; quality is a value from 0 to 100 for formats where it applies, and does not apply to PNG. Check the installed Puppeteer version’s options reference for supported types and details.

// JPEG with quality setting
await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 80 });

// Capture a rectangle in CSS pixels
await page.screenshot({
  path: 'region.png',
  clip: { x: 100, y: 120, width: 800, height: 500 },
});

See the Puppeteer screenshots guide and ScreenshotOptions reference for the available options and version-specific behavior.

5. Complete runnable alternatives

If you prefer another language for orchestration, these examples call the ScreenshotNeo API to capture a publicly reachable URL; they do not automate a login or access a private browser session. For a protected site, use the Puppeteer workflow above or a supported authenticated capture configuration. Keep API keys in environment variables.

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,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. For a URL it can return a PNG, JPEG, WebP, or PDF; see the API documentation for configuration. Here is the one-call form:

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

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots. Those API calls capture URLs and do not reuse a Puppeteer browser session or log in to an account for you.

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

Performance, reliability, and cost

  • Wait for the needed state. A precise selector is usually a better readiness check than a long arbitrary delay. Avoid waiting for every network connection to become idle if the app keeps analytics or streaming requests open; wait for the content required in the image.
  • Keep browser work bounded. Close pages and browsers in finally blocks so failures do not leave Chromium processes running. Set reasonable timeouts and surface failures rather than silently saving a login screen.
  • Use the smallest capture that serves the task. A viewport or element capture uses less image area than a tall full-page result. Full-page captures can be large and may take longer to render and write.
  • Account for HTTP authentication overhead. Puppeteer documents that page.authenticate() turns on request interception internally, which may affect performance.
  • Secure secrets and outputs. Restrict access to screenshots containing private information, avoid logging credentials and cookies, and use the site’s approved authentication method.
  • Plan API cost separately from browser cost. A self-managed Puppeteer run uses your own browser compute and infrastructure. ScreenshotNeo bills clean shots only; its supplied plans are Free (1,000/month), Starter ($5 for 3,000), Growth ($15 for 15,000), Pro ($39 for 60,000), Scale ($99 for 250,000), and Business ($249 for 1,000,000). Yearly billing gives two months free, and every feature is on every plan. Check the product documentation for current configuration details.

Troubleshooting

Symptom Likely cause Fix
The screenshot shows the sign-in page Login failed, the site redirected back, or the capture started before the protected state appeared. Check the final URL and wait for a site-specific signed-in marker and protected content before capture.
The login click sometimes times out or misses the page change The navigation wait was registered after the click, or the app uses client-side routing. Start waitForNavigation() and the click together with Promise.all(); for an SPA, wait for the resulting signed-in marker too.
The cookie is set but access is still denied The cookie has the wrong domain or path, is expired, lacks a required attribute, or the server has revoked the session. Use the site’s valid cookie data and correct scope; obtain a fresh authorized session if needed.
The page URL changed but content is missing A History API route transition completed before data or protected UI rendered. Wait for the actual heading, component, or app state needed for the screenshot.
A selector wait times out The selector is wrong, the element is hidden, or the site has not reached that state. Inspect the authorized page, use its real selector, and distinguish visible from merely attached elements.
The capture is cropped or much taller than expected The chosen mode is viewport, full document, clip rectangle, or element capture. Choose the mode deliberately; adjust viewport or clip coordinates, or capture the specific element.
Output behavior differs across environments The installed Puppeteer version or browser may differ from the API docs reviewed. Check the package version and its matching Puppeteer documentation.

Frequently asked questions

Can Puppeteer bypass a login or CAPTCHA?

This workflow automates an authorized login or restores a valid session; it does not make an invalid session valid or provide a general CAPTCHA bypass. Follow the site’s access and authentication requirements.

Is page.authenticate() for a normal sign-in form?

No. It handles HTTP authentication challenges. For an HTML form, fill and submit the site’s fields and verify the signed-in state.

Does fullPage: true capture the entire document?

It requests a full-document screenshot instead of the default viewport capture. For a component or bounded region, use an element screenshot or clip rectangle.

Can I use ScreenshotNeo with a page that requires my Puppeteer login?

The one-call URL API does not reuse your local Puppeteer session. Use the browser workflow when the page depends on an interactive authenticated session; consult ScreenshotNeo’s documentation for its supported request options.

References