How to Screenshot Multiple URLs That Use Single Sign-On
Sign in once, save Playwright’s authenticated browser state, then reuse it to capture multiple URLs. Includes runnable code, security guidance, and troubleshooting.
Direct answer: Use browser automation to sign in once, save the authenticated browser state, load that state in a new browser context, and visit each target URL to save its screenshot. The example below uses Playwright with Node.js. It works when the target application accepts the saved state for those URLs; it does not bypass MFA, CAPTCHA, access policies, or other sign-in challenges.
Playwright’s authentication guide documents saving and reusing browser state. Its screenshot guide covers page screenshots and their options. Treat the saved state like a credential: it may allow someone to impersonate the signed-in account.
1. Prepare the capture
- Choose an account that is authorized to access every target page.
- Check whether the URLs share a domain, identity provider, and access policy. A state file accepted by one application or domain may not authenticate another.
- Choose a stable browser and execution environment if you need comparable screenshots over time.
- Install Playwright and create a private directory for the state file and captured images.
npm init -y
npm install playwright
npx playwright install chromium
mkdir -p auth screenshots
Add the authentication directory to .gitignore before signing in:
printf '\nauth/\n' >> .gitignore
Do not commit auth/state.json. If your team needs to share captures, share the resulting images through an approved channel, not the reusable authentication state.
2. Sign in once and save browser state
Create login.js. The example opens a visible browser so you can complete the site’s normal sign-in flow, including any required identity-provider prompts or MFA. Change the URL and the success condition to match your application. Save the state only after the final redirect and a recognizable signed-in element are present.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto('https://app.example.com/login', {
waitUntil: 'domcontentloaded',
});
// Complete the site's approved sign-in flow in the browser window.
// Wait for a reliable, app-specific signal that sign-in has finished.
await page.waitForURL('https://app.example.com/**', { timeout: 120000 });
await page.getByRole('navigation', { name: 'Main navigation' }).waitFor({
state: 'visible',
timeout: 30000,
});
await context.storageState({
path: 'auth/state.json',
indexedDB: true,
});
console.log('Saved authenticated state to auth/state.json');
} finally {
await browser.close();
}
})();
The URL pattern and navigation locator are examples. Replace them with a final URL or page element that is only available after successful authentication. Waiting only for a redirect can save state too early if the application is still setting cookies or finishing its sign-in flow. Playwright’s current storage-state options can include IndexedDB; use the option supported by your installed Playwright version and application.
node login.js
3. Restore state and capture each URL
Create capture.js. It loads the saved state into a browser context, visits each URL in sequence, checks that an expected signed-in element is visible, and writes one full-page PNG per URL. The check is important: a screenshot can be produced successfully even when the session has expired and the page redirected to a login screen.
const { chromium } = require('playwright');
const path = require('node:path');
const targets = [
{ name: 'overview', url: 'https://app.example.com/reports/overview' },
{ name: 'billing', url: 'https://app.example.com/settings/billing' },
{ name: 'team', url: 'https://app.example.com/settings/team' },
];
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
storageState: 'auth/state.json',
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
try {
const page = await context.newPage();
for (const target of targets) {
const response = await page.goto(target.url, {
waitUntil: 'domcontentloaded',
timeout: 60000,
});
if (response && !response.ok()) {
throw new Error(`${target.url} returned HTTP ${response.status()}`);
}
// Replace with an element that proves this URL is signed in.
await page.getByRole('navigation', { name: 'Main navigation' }).waitFor({
state: 'visible',
timeout: 30000,
});
await page.screenshot({
path: path.join('screenshots', `${target.name}.png`),
fullPage: true,
animations: 'disabled',
});
console.log(`Saved ${target.name}.png (${page.url()})`);
}
} finally {
await context.close();
await browser.close();
}
})();
node capture.js
Use unique, filesystem-safe names rather than deriving paths directly from arbitrary URLs. If you want to capture an element instead of a whole page, locate it and call locator.screenshot({ path: 'screenshots/section.png' }). For test suites, Playwright also provides screenshot assertions through its test runner; those compare output and are a different use from simply saving an image.
4. Choose waits and screenshot options
There is no single wait that fits every application. Pick a wait that represents the page state you intend to capture, and use an application-specific selector when possible.
| Need | Playwright choice | Notes |
|---|---|---|
| Initial document loaded | waitUntil: 'domcontentloaded' |
Often a useful starting point for apps that continue fetching data after navigation. |
| All load events completed | waitUntil: 'load' |
Can wait longer when pages load many resources. |
| Signed-in content is ready | locator.waitFor() or a web-first locator assertion |
Usually the most meaningful signal; select an element that distinguishes the real page from login and error states. |
| Known short animation or delayed content | page.waitForTimeout(milliseconds) |
Use only when the delay is intentional and understood; fixed sleeps add time and can still be too short. |
| Full page image | page.screenshot({ fullPage: true }) |
Captures beyond the current viewport; very tall pages can consume more memory and produce large files. |
| Stable animation frame | animations: 'disabled' |
Useful for reducing animation variation in captures. |
| Different format | Use a supported screenshot format and matching filename extension | PNG is the default; Playwright visual comparison documentation also describes WebP output by extension. |
Playwright’s visual comparison documentation notes that screenshots can differ with the host operating system, browser version, settings, hardware, power conditions, and headless mode. Keep those conditions stable when comparing images.
5. Understand which authentication state is reused
Applications may store sign-in data in cookies, local storage, IndexedDB, or passkeys/WebAuthn. Playwright’s storage-state workflow covers common reusable browser state, with IndexedDB included when requested and supported. The target application still decides whether that state is valid.
Session storage is a special case. It is scoped to a page session and is not included in Playwright’s standard storage-state persistence. Playwright documents a custom save-and-restore approach for applications that rely on it. If you use that approach, scope the data to the correct origin and handle it with the same care as cookies.
Saved state is not permanent. It can expire, be revoked, or become invalid after an account or organization policy changes. If any target returns a login page, sign in again and regenerate the state file.
6. Handle multiple domains and protected routes
- Same application and domain: One state file is often sufficient when the app shares the same authentication session across routes.
- Several subdomains: Check cookie domain and identity-provider behavior. A cookie scoped to one host may not be sent to another.
- Different applications or tenants: Authenticate for each permitted app or tenant and maintain separate state files if their sessions differ.
- Access denied after sign-in: Authentication proves identity, not authorization. Confirm the account has permission for that page.
- MFA, CAPTCHA, or conditional access: Complete interactive challenges according to the site owner’s process. Saving state is not a universal way to skip them.
- Passkeys/WebAuthn: Support depends on how the application and browser use credentials. Confirm the chosen flow works in the browser environment you will run.
When the application supports an approved API authentication flow, that may be another way to establish a browser context. Playwright documents API-based authentication patterns, but whether a particular SSO-protected site permits them depends on that site’s design and policy.
7. Secure the state and captured images
- Keep the auth directory out of version control and restrict filesystem access.
- Do not paste state contents into logs, issue trackers, chat, or build output.
- Use a dedicated, least-privilege account where the site supports one.
- Store state in an approved secret store for CI and write it to a temporary protected location at runtime.
- Delete or rotate state when it expires, is no longer needed, or may have been exposed.
- Review screenshots for personal, financial, or otherwise sensitive data before sharing them.
Playwright warns that saved state may contain sensitive cookies and headers that could be used to impersonate the account. Treat it like a password.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Capture shows a login page | State was saved before sign-in settled, expired, or is not valid for this domain. | Wait for a post-login signal before saving; reauthenticate; confirm cookie scope and origin. |
| Login script times out on the URL wait | The app redirects to a different final URL, or its callback flow takes longer. | Inspect the final URL manually and change the pattern; wait for a stable authenticated element instead. |
| State works locally but not in CI | Different origin, browser setup, environment policy, missing state file, or expired session. | Confirm the file is securely provisioned, use a compatible browser setup, and regenerate state through the approved sign-in flow. |
| One URL works and another does not | Different host, tenant, permissions, or application session. | Verify access in a normal browser and check whether another state file or authorized sign-in is needed. |
| Page loads but screenshot is blank or incomplete | App content renders after navigation, or a selector/wait does not represent readiness. | Wait for the relevant content locator and inspect the page before capture; avoid relying only on a fixed delay. |
| Images differ between runs | Browser or host rendering conditions, dynamic content, animations, or fonts changed. | Pin the execution environment, disable animations where appropriate, and mask or stabilize dynamic regions if using screenshot assertions. |
| Session-storage application remains signed out | Standard storage state does not preserve session storage. | Implement Playwright’s documented custom session-storage save-and-restore pattern for the correct origin. |
| Access denied despite a successful login | The account is authenticated but lacks authorization for the resource. | Ask the site owner for the appropriate access; do not treat screenshot automation as an access-control workaround. |
9. Performance, reliability, and cost
For a small URL list, sequential navigation is easy to reason about and limits load on the target application. Each page adds its own navigation, readiness wait, and image-write time. Reuse one browser process and authenticated context rather than launching a browser for every URL. For larger lists, add bounded concurrency only after confirming the site permits the request rate; too many simultaneous pages can increase memory use and trigger rate limits.
Set explicit navigation and selector timeouts, log the URL and final page URL for each capture, and keep failures associated with their target. A failed navigation should not silently produce an image that appears successful. Retry only transient failures, with a limit and backoff; do not repeatedly retry sign-in challenges or authorization denials.
Playwright is browser automation software, so cost depends on where and how you run it: local compute, CI minutes, storage, and any infrastructure you operate. This workflow does not imply a particular runtime price or capture speed. Keep the browser version and environment stable if image consistency matters.
10. Or skip the browser setup
If you do not want to manage browser installation and reusable browser state for ordinary public pages, ScreenshotNeo provides a website screenshot API. It is not a replacement for authenticating to private SSO pages: use the Playwright workflow above when access depends on your signed-in browser session.
One GET request returns an image or PDF. See the ScreenshotNeo API documentation for parameters and configuration.
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);
- Cookie banners, popups, and chat widgets are removed before the shot; those steps can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers say the page verdict and billing status.
- An MCP server offers
take_screenshot,get_page_info, andcapture_pdffor AI agents and MCP clients. - 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Learn about ScreenshotNeo or sign up free for 1,000 screenshots a month, with no card.
11. FAQ
Can one saved state file work across every SSO provider?
No. It works only where the application accepts the stored state and the account is authorized. Provider configuration and site policy determine compatibility.
Does a screenshot prove that a page was captured while signed in?
No. Validate the final URL and an authenticated page element, and review the image. Automation can otherwise capture a login, error, or access-denied page.
Can I use this workflow for visual regression checks?
Yes. Keep browser and host conditions stable, and use Playwright’s screenshot assertions when you want automated comparisons in a test suite.
Should the state file be shared with teammates?
Only through an approved secure mechanism and when necessary. It can function as an account credential; sharing the resulting screenshots is usually sufficient.


