How to Screenshot a Website Behind a Login Without Capturing the Sign-In Page
Use an authenticated Playwright session, confirm the page is signed in, then capture the viewport, full page, or a selected element.
To screenshot a page behind a login, capture it with an authorized browser session that is already signed in. With Playwright, log in once, save the authenticated browser state, load it in the capture context, navigate to the protected page, verify the final URL and a signed-in page element, then take the screenshot. Waiting for those checks prevents a redirect or unfinished login from leaving you with an image of the sign-in form.
This guide uses Playwright with Node.js. It also covers session storage, capture scope, masking, security, troubleshooting, and a hosted alternative.
1. Install Playwright and prepare the auth-state directory
Install Playwright and its Chromium browser. Keep the saved state in a dedicated directory and exclude it from source control: it contains credentials that may let someone impersonate the account.
npm init -y
npm install --save-dev playwright
npx playwright install chromium
mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore
Use a test account or another account you are authorized to access. Do not save authentication state from an account you do not control.
2. Sign in once and save browser state
Create save-auth.mjs. Replace the example login URL and selectors with the ones for your site. The script waits for the post-login destination and checks a signed-in landmark before saving state.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Email').fill(process.env.SITE_EMAIL);
await page.getByLabel('Password').fill(process.env.SITE_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
// Use the real post-login URL pattern for your site.
await page.waitForURL('**/dashboard', { timeout: 30_000 });
await page.getByRole('heading', { name: 'Dashboard' }).waitFor({ state: 'visible' });
await context.storageState({ path: 'playwright/.auth/user.json' });
console.log('Saved authenticated browser state.');
} finally {
await browser.close();
}
Set SITE_EMAIL and SITE_PASSWORD in your shell or secret manager before running the script. Avoid putting credentials directly in source code or command history. If the site uses multi-factor authentication, complete it as part of the authorized login flow before saving state.
SITE_EMAIL='you@example.com' SITE_PASSWORD='your-secret' node save-auth.mjs
Playwright’s authentication guide describes reusable browser state and recommends keeping auth files out of version control. Browser state is sensitive authentication material, not a harmless screenshot setting.
3. Load the saved state, verify access, and capture
Create capture.mjs. This example captures the whole page, masks an account control that may display private information, verifies the destination, and checks the signed-in landmark before writing the image.
import { chromium } from 'playwright';
const target = 'https://example.com/dashboard/reports';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
try {
await page.goto(target, { waitUntil: 'domcontentloaded' });
await page.waitForURL('**/dashboard/reports', { timeout: 30_000 });
await page.getByRole('heading', { name: 'Reports' }).waitFor({ state: 'visible' });
await page.screenshot({
path: 'authenticated-page.png',
fullPage: true,
animations: 'disabled',
mask: [page.getByTestId('account-menu')],
maskColor: '#000000',
});
console.log(`Captured ${page.url()}`);
} finally {
await browser.close();
}
Run it with node capture.mjs. Replace the URL, route pattern, heading, and mask locator with site-specific values. If the account control is not sensitive or is not present, remove the mask option. Inspect the output: masking covers locator boxes and can obscure useful content if the locator is too broad.
Playwright documents screenshot methods, full-page capture, clipping, and masking in its Page API reference and screenshot tool documentation.
4. Choose the right screenshot scope
| Scope | Playwright example | Use it when |
|---|---|---|
| Viewport | await page.screenshot({ path: 'view.png' }) |
The relevant content fits in the current browser window. |
| Full page | await page.screenshot({ path: 'full.png', fullPage: true }) |
You need the complete scrollable page. This can produce very tall images. |
| One element | await page.getByTestId('report-chart').screenshot({ path: 'chart.png' }) |
Only a chart, table, panel, or other component is relevant. |
| Clipped rectangle | await page.screenshot({ path: 'clip.png', clip: { x: 0, y: 0, width: 900, height: 600 } }) |
You need a fixed area of the page. Coordinates are relative to the page. |
Use the narrowest scope that answers the task. Full-page capture cannot be combined with targeting a single element. For pages that load images or content as you scroll, ensure those resources have loaded before capturing; a screenshot taken too early may contain placeholders or incomplete sections.
5. Make capture repeatable
- Wait for the final URL after sign-in and after navigating to the target. Sites can redirect several times while establishing the session.
- Wait for a stable page-specific landmark such as a heading, account control, or report identifier. Prefer a selector tied to the page’s meaning over a fixed sleep.
- Use a fixed viewport when comparing captures over time. Choose a device scale factor deliberately because it affects image dimensions.
- Disable animations when a stable frame matters. For content that updates asynchronously, wait for the specific content to appear or for a known loading indicator to disappear.
- For infinite scroll or lazy-loaded sections, scroll through the page and wait for the content before using full-page capture. Full-page capture alone does not guarantee every lazy resource has been requested.
- Capture only the information needed. Mask private account fields or select a smaller element, then inspect the saved artifact before sharing it.
6. Handle session storage and other authentication edge cases
Playwright’s reusable state supports cookies, local storage, IndexedDB, and passkey-based authentication. Session storage is a special case: it is domain-specific and is not persisted across page loads by the usual storage-state workflow. If a site depends on it, use a site-specific setup that initializes that storage in the page before application code runs, or keep the authenticated browser context alive for the capture instead of expecting the saved state file to restore it automatically.
Some sites bind sessions to a browser, device, or other context. Do not assume state saved in one browser configuration will work in another. When reuse fails, authenticate in the same browser and context configuration that will perform the capture, if the site’s supported flow permits it.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot shows the sign-in page | Login did not finish, the session expired, or navigation redirected to login. | Wait for the final URL and a signed-in landmark before capturing. Check page.url() and fail the run if it points to the login route. |
| Saved state still redirects to login | The site uses session storage, the state expired, or authentication is browser-specific. | Identify the storage mechanism, reauthenticate, and handle session storage explicitly. Try capturing in the same browser configuration used to sign in. |
| Timeout waiting for URL or heading | The expected route or selector differs, or the site did not complete authentication. | Inspect the actual URL and page content during a local run. Update the URL pattern and landmark to match the site’s real signed-in page; check for failed credentials or an unfinished second-factor prompt. |
| Screenshot is blank or incomplete | The page was captured before its content loaded, or a client-side request failed. | Wait for a meaningful element or known loading state to complete. Check that the target page works in the same browser context. |
| Image is too tall or includes unrelated content | Full-page mode captures more than the useful region. | Use viewport, an element screenshot, or a clip rectangle. |
| Private details remain visible | The mask locator missed the sensitive field, or the screenshot scope includes another private area. | Use a more precise locator or narrower capture scope, then inspect the resulting image before sharing it. |
| Private details are covered along with useful content | The masked locator’s bounding box is too large. | Target a smaller element, mask only the sensitive field, or capture the relevant component separately. |
8. Security, reliability, and cost considerations
Protect the session
Playwright warns that a browser state file may contain sensitive cookies and headers that could be used to impersonate the account. Keep it out of source control, restrict access to it and to screenshot artifacts, and delete or rotate it when it is no longer needed. Treat screenshots as potentially sensitive too: the visible page may contain names, records, or customer information.
Make runs fail clearly
A screenshot file existing on disk does not prove that the capture reached the intended page. Validate the final URL and a signed-in landmark before capture. For scheduled or bulk work, log the target URL, final URL, capture time, and failure reason without logging passwords, cookies, or the storage-state contents. Retry transient navigation failures only after deciding whether the action is safe to repeat.
Keep work and cost proportional
Browser automation uses a browser process and takes longer than a direct image request, especially when each run must authenticate and load a large page. Reusing authenticated state avoids repeating the interactive login, but the state must remain secure and valid. A viewport or element capture creates less output than a full-page image. For recurring captures, account for browser runtime, artifact storage, and the operational work of keeping selectors and authentication flows current.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. It takes one GET request and returns an image or PDF. Use it for public pages; it does not replace an authorized signed-in browser session for capturing private pages.
For a public page, try this call. See the ScreenshotNeo documentation for options and setup.
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)
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);
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. Every feature is on every plan. Visit ScreenshotNeo or sign up free for 1,000 screenshots a month, with no card.
FAQ
Why does my screenshot keep showing the login page?
The browser has not reached or retained an authenticated session. Check the final URL, wait for a signed-in element, and confirm the site’s session storage requirements.
Can I use a screenshot API for a private page?
A normal URL-only request cannot use your existing browser session automatically. For a private page, use an authorized authenticated browser context, or a service that explicitly supports the required authentication method.
Should I capture the whole page?
Only if below-the-fold content matters. A viewport or element capture is often easier to review and exposes less unrelated account information.
Can I share the saved auth-state file?
Avoid sharing it. It may contain credentials capable of restoring an authenticated session.


