How to Take Screenshots of a Website That Requires Login with Chrome Headless
Use Puppeteer with Headless Chrome to authenticate, confirm protected content has loaded, and capture a reliable screenshot. Includes sessions, CLI, troubleshooting, and an API shortcut.
Direct answer: Headless Chrome can capture a page that requires login once the browser has an authorized authenticated session. For repeatable captures, use Puppeteer: log in through the site’s normal flow or load a dedicated authenticated browser profile, navigate to the protected page, wait for a marker that proves the signed-in content is ready, then call page.screenshot(). Headless mode does not log you in by itself.
This guide uses Puppeteer and current regular Headless Chrome. It covers form login, persistent profiles, isolated contexts, HTTP authentication, the Chrome command-line option, and reliable capture checks. Use only accounts and pages you are permitted to access.
1. Choose the right authentication approach
| Approach | Use it when | Session behavior |
|---|---|---|
| Site’s normal login flow | The job starts signed out, and the site supports the expected browser login flow. | Sign in, verify a signed-in marker, then capture. MFA and SSO behavior is site-specific. |
Dedicated persistent userDataDir |
An authorized automation account needs to stay signed in between runs. | Cookies and other browser profile data persist on disk. Protect the directory like a secret. |
| Isolated BrowserContext | Jobs need separate cookies and local storage, or a session should be discarded after a run. | Storage is scoped to the context. Close it when finished. |
| HTTP authentication | The server presents a browser-level HTTP authentication challenge. | page.authenticate() supplies HTTP credentials; it does not fill a website’s HTML login form or complete SSO. |
Choose one of the first three for ordinary website login. Do not copy cookies from an unrelated user’s browser or put session values in source control, logs, or a public screenshot.
2. Install Puppeteer and prepare secrets
Use a supported Node.js version and install Puppeteer in a project directory:
npm init -y
npm install puppeteer
Set credentials in your job runner’s secret store or environment. For a local shell, for example:
export SITE_USER='your-automation-user'
export SITE_PASSWORD='your-secret'
Do not commit a .env file containing real credentials. The example below uses placeholder selectors and URLs; replace them with the login fields, submit control, and a reliable signed-in content marker for your site. There is no universal form-login script: identity-provider redirects, MFA, and session rules vary.
3. Log in and capture with Puppeteer
Save as capture.mjs and run with node capture.mjs. This example starts signed out, follows a site-specific form flow, checks for authenticated content, and saves a full-page PNG.
import puppeteer from 'puppeteer';
const loginUrl = 'https://example.com/login';
const targetUrl = 'https://example.com/account/report';
const outputPath = 'authenticated-page.png';
const username = process.env.SITE_USER;
const password = process.env.SITE_PASSWORD;
if (!username || !password) {
throw new Error('Set SITE_USER 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(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();
// Replace with a stable marker that only appears after successful sign-in.
await page.waitForSelector('[data-testid="signed-in-marker"]', {
visible: true,
timeout: 15000,
});
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
// Confirm the protected content itself is present before taking the image.
await page.waitForSelector('[data-testid="report-content"]', {
visible: true,
timeout: 20000,
});
await page.screenshot({ path: outputPath, fullPage: true });
console.log(`Saved ${outputPath}`);
} finally {
await browser.close();
}
The selectors, URLs, and marker are examples, not universal selectors. If the form redirects to an identity provider, automate only the site’s intended flow and add checks for the expected redirect and final signed-in state. If the site requires an interactive MFA challenge that cannot be completed by the authorized job, use its approved automation or session process instead of trying to bypass the challenge.
Start with an already authenticated profile
For scheduled captures where the authorized session is intentionally reused, launch with a dedicated profile directory. Sign into that profile through the site’s expected flow once, then let the job reuse it. Do not use a personal everyday Chrome profile in an unattended process.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
userDataDir: './private-automation-profile',
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000 });
await page.goto('https://example.com/account/report', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('[data-testid="report-content"]', {
visible: true,
timeout: 20000,
});
await page.screenshot({ path: 'report.png', fullPage: true });
} finally {
await browser.close();
}
The profile directory stores reusable browser state, including session data. Restrict filesystem access, keep it out of source control and shared artifacts, and use a dedicated automation identity. A profile is convenient for persistence; it is not an encrypted credential vault.
Use an isolated browser context per job
When jobs must not share cookies or local storage, create a separate context and close it after capture. Perform the site’s authorized login inside that context before navigating to the protected page.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
await page.setViewport({ width: 1440, height: 1000 });
// Perform the site's normal login flow in this context if needed.
await page.goto('https://example.com/account/report', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('[data-testid="report-content"]', {
visible: true,
timeout: 20000,
});
await page.screenshot({ path: 'isolated-report.png', fullPage: true });
} finally {
await context.close();
await browser.close();
}
HTTP authentication is a separate case
For a genuine HTTP authentication challenge, configure the page before navigation:
await page.authenticate({
username: process.env.HTTP_USER,
password: process.env.HTTP_PASSWORD,
});
await page.goto('https://protected.example.com/report', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('[data-testid="report-content"]', { visible: true });
await page.screenshot({ path: 'http-auth-report.png', fullPage: true });
Puppeteer’s page.authenticate() is for HTTP authentication, not a generic form fill. It enables request interception behind the scenes, which can affect performance.
4. Wait for the right page state
A navigation event only says something about loading; it does not prove that the authenticated report rendered. After navigation, wait for a page-specific element that appears only in the intended signed-in view. If the application renders data asynchronously, wait for the data container or another stable completion signal rather than guessing with a long fixed delay.
- Set the viewport before navigating when the screenshot must match a desktop or mobile layout. Use the same viewport for repeatable captures.
- Check the final URL after redirects. A URL that ends at
/loginusually means the session did not reach the target. - Use an application marker such as an account heading or report container. Avoid treating
networkidlealone as proof of readiness on apps with analytics, polling, or other background requests. - Use bounded timeouts and report a useful error if the marker never appears; silent or unbounded waits make scheduled jobs hard to diagnose.
5. Choose viewport, full-page, or element capture
By default, page.screenshot() captures the visible viewport. Use fullPage: true for content beyond the viewport, or capture a specific element when only one report panel is needed.
// Viewport only
await page.screenshot({ path: 'viewport.png' });
// Entire page
await page.screenshot({ path: 'full-page.png', fullPage: true });
// One element
const report = await page.waitForSelector('[data-testid="report-content"]', {
visible: true,
});
await report.screenshot({ path: 'report-element.png' });
Full-page images can become very tall and consume more memory. Prefer viewport or element capture when that matches the task. If the page lazy-loads content as it scrolls, ensure the required content is loaded before capture; a full-page flag does not guarantee every site’s lazy content has rendered.
6. Use Chrome Headless from the command line
For a simple one-off capture of a page that is already publicly accessible to the browser, Chrome’s CLI can write a screenshot to the current directory:
chrome --headless --screenshot --window-size=1280,900 https://example.com
The Chrome CLI is convenient for a basic capture, but it does not provide the same straightforward interactive login and application-specific readiness flow as Puppeteer. For login-protected pages, establish the authorized session in a browser profile or use Puppeteer to perform the flow and verify the resulting page before capture. The command above alone does not authenticate you.
7. Cookies, sessions, and safe handling
Cookies are only one part of browser session state; a site may also rely on local storage or other browser state. When using session cookies legitimately, preserve their scope and relevant attributes, such as domain, expiry, httpOnly, secure, and sameSite. Current Puppeteer supports cookies through browser or browser-context methods; the older page-level cookie API is deprecated.
- Prefer the site’s normal login flow or a dedicated profile over manually copying raw session values.
- Do not hard-code credentials or cookie values, print them in logs, or include them in screenshots.
- Use a separate profile or context for automation rather than sharing an interactive personal session.
- Keep captured images private when they contain account, customer, or business information.
- Expect sessions to expire or be revoked; check for the signed-in marker on every run.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot shows the login page | Credentials were rejected, a redirect dropped the session, or the target requires a different sign-in flow. | Check the final URL and visible signed-in marker after login. Confirm the same page/context or profile is used for the target navigation. |
| Screenshot shows a shell, spinner, or blank area | The capture ran before the protected content finished rendering, or a request failed. | Wait for a site-specific content marker. Inspect console and network failures; confirm the account has access to that page. |
| Selector timeout | The example selector does not match the site, is hidden, or the login did not succeed. | Inspect the page DOM and replace the placeholder with a stable selector. Check the final URL and whether the element is visible. |
| Login works manually but fails headless | The site may use a different redirect, MFA, or interaction path, or the script’s selectors and timing do not match it. | Use the site’s expected flow, inspect each redirect and error, and use an approved persistent session if the job requires it. Do not bypass access controls. |
| Session disappears between runs | A fresh browser profile/context is created each time, or the session expired. | Use a dedicated persistent userDataDir if reuse is intended, or deliberately log in per isolated context. Re-check authorization and session expiry. |
| HTTP credentials do not fill the login form | page.authenticate() handles HTTP authentication challenges only. |
Use the website’s form or identity-provider flow for HTML login. |
| Capture is clipped or has the wrong layout | Viewport dimensions differ from the desired layout, or viewport capture was used for a full document. | Set the viewport before navigation; choose fullPage: true or an element screenshot where appropriate. |
| Script hangs or is very slow | It may be waiting for network idle on a page with ongoing traffic, or an unbounded wait has no failure path. | Use a bounded timeout and a meaningful page marker. Review failed requests and avoid relying on network quiet alone. |
9. Performance, reliability, and cost
For repeated captures, reuse a dedicated profile only when session persistence is needed; use isolated contexts when separation and cleanup matter more. Keep the browser lifecycle bounded with try/finally, set explicit timeouts, and capture only the area needed. Full-page screenshots and HTTP-auth request interception may add work. A profile improves convenience but adds responsibility for protecting stored session data.
Chrome and Puppeteer are software you run, so there is no per-screenshot service charge in this workflow; account for your own compute, storage, and operational time. Failures still cost debugging and rerun time. Log outcome details such as final URL and which readiness check failed, but never log credentials, cookies, or sensitive page content.
10. Current Headless Chrome modes
Current Puppeteer uses regular Headless Chrome by default with headless: true. Puppeteer also documents a separate chrome-headless-shell mode selected with headless: 'shell'; it may be faster for automation that does not need the full browser feature set, but it does not fully match regular Chrome behavior. Chromium’s current documentation says that from M132, headless shell is no longer part of the Chrome binary and --headless=old has no effect. Use regular Headless Chrome unless you specifically need and have installed the separate shell.
Or skip the browser setup
If you need a screenshot from a public URL and do not need to sign into a private account, ScreenshotNeo offers a one-request API. It is a website screenshot API and MCP server from ScreenshotNeo. See the API documentation. This API example is for a public page; it is not a way to submit a website’s login form or capture a private authenticated session.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo removes cookie banners, popups, and chat widgets 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; paid plans start at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.
FAQ
Does Chrome Headless bypass a login?
No. It renders pages using the browser’s current session. You still need an authorized login or session.
Can I use this for an SSO-protected page?
Sometimes, if the site’s expected sign-in flow can be completed and the session is valid in the browser context. The exact redirects, MFA, and session rules depend on the site.
Should I use a persistent profile or a new context?
Use a persistent, protected profile when runs intentionally reuse a session. Use a fresh context when jobs need storage isolation and session cleanup.
Can ScreenshotNeo capture my private account page?
The example above is for a public URL. For an authenticated page, use an authorized browser session with Puppeteer or another approved workflow.


