ScreenshotNeo

BlogHow-to

Best Way to Store Login State for Scheduled Screenshots of a Private Website

Use Playwright storageState to reuse an authenticated session for scheduled screenshots, while protecting the file, handling expiry, and restoring sessionStorage when needed.

By the ScreenshotNeo team4 October 202610 min read

For most Playwright screenshot jobs, authenticate in a setup step, save the browser context with storageState, and load that state into a fresh context for each scheduled capture. Treat the saved file like a password: it can contain cookies and headers that let someone impersonate the account. Keep it out of source control and logs, restrict access to the screenshot job, and refresh it when the site invalidates the session. If the site depends on sessionStorage, save and restore that separately.

This guide uses Playwright because its official documentation describes this setup and reuse pattern. The exact state lifetime, MFA behavior, and secret-storage configuration depend on the target site and the CI environment.

1. Decide how the job should authenticate

Use saved state when the site’s authentication is represented by browser state that Playwright can persist and reauthentication on every run is unnecessary. The documented state covers cookies, local storage, IndexedDB, and passkey-based authentication. It does not automatically persist sessionStorage.

Choose between these patterns based on the site’s policy and your job:

Approach Use it when Trade-off
Reuse saved storageState The site supports session reuse and scheduled captures should begin already signed in. The artifact is credential-like, can expire, and needs controlled access and a refresh process.
Log in during every run Sessions should be short-lived or the site’s policy requires fresh authentication. Each run has more steps. MFA, CAPTCHA, or interactive controls may prevent unattended login.

Use the site’s supported authentication flow. If a suitable login API exists, Playwright can authenticate through an API request and save the resulting state. Otherwise, complete the UI login, then verify that authentication succeeded before saving. For UI login, wait for a known signed-in element or the expected final URL; do not assume that clicking Submit means the session is ready.

2. Install Playwright and prepare a state directory

The following example uses Playwright’s JavaScript API. Install the package and browser in your project:

npm install playwright
npx playwright install chromium

Create a directory for the state artifact and exclude it from Git. For example, add this entry to .gitignore:

playwright/.auth/

Do not put credentials or the resulting state file in source control, screenshot output, or CI logs. Store it as a protected CI artifact or secret using the access controls available in your environment. Limit access to the job that needs it, and avoid printing the file or its contents during debugging.

3. Authenticate once and save the state

This runnable example signs in through a form, waits for a signed-in marker, then saves the authenticated context. Replace the example URL, selectors, and environment variable names with those for your site. Provide credentials through your CI secret mechanism, not by hard-coding them.

// save-auth.mjs
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';

const baseURL = process.env.PRIVATE_SITE_URL;
const username = process.env.PRIVATE_SITE_USERNAME;
const password = process.env.PRIVATE_SITE_PASSWORD;
if (!baseURL || !username || !password) {
  throw new Error('Set PRIVATE_SITE_URL, PRIVATE_SITE_USERNAME, and PRIVATE_SITE_PASSWORD');
}

const authDir = 'playwright/.auth';
await mkdir(authDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();

try {
  await page.goto(new URL('/login', baseURL).toString(), { waitUntil: 'domcontentloaded' });
  await page.getByLabel('Email').fill(username);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Replace this with a reliable post-login signal for the target site.
  await page.getByTestId('account-menu').waitFor({ state: 'visible', timeout: 30_000 });
  await context.storageState({ path: `${authDir}/state.json` });
} finally {
  await browser.close();
}

Run this as a separate setup step when the state is missing or expired. For a site with an appropriate API login, use the site’s supported API flow and Playwright’s API request context to authenticate, then save that request context’s storage state. Do not assume that a generic form or API endpoint exists; adapt to the site’s documented authentication method.

4. Load the saved state for a scheduled screenshot

Create a new browser context for the capture and pass the saved state when creating it. A fresh context per job gives the run its own page and browser session while reusing the authentication artifact.

// capture.mjs
import { chromium } from 'playwright';

const baseURL = process.env.PRIVATE_SITE_URL;
if (!baseURL) throw new Error('Set PRIVATE_SITE_URL');

const browser = await chromium.launch({ headless: true });
try {
  const context = await browser.newContext({
    storageState: 'playwright/.auth/state.json',
    viewport: { width: 1440, height: 1000 },
  });
  const page = await context.newPage();
  await page.goto(new URL('/reports/daily', baseURL).toString(), {
    waitUntil: 'domcontentloaded',
    timeout: 30_000,
  });

  // Check authentication before capturing; replace the marker with one from your site.
  await page.getByTestId('account-menu').waitFor({ state: 'visible', timeout: 15_000 });
  await page.screenshot({ path: 'artifacts/daily-report.png', fullPage: true });
  await context.close();
} finally {
  await browser.close();
}

Install and run with node save-auth.mjs during the setup or refresh step, then node capture.mjs in the scheduled job. Ensure the state file is available at the configured path before starting the capture. Keep generated screenshots under the access policy appropriate for the private content they contain.

5. Handle sessionStorage separately when the site needs it

Playwright’s storageState API does not persist sessionStorage. If the application stores an essential login value there, serialize only the relevant origin’s values and restore them before the page’s scripts run. The following illustrates the mechanism; limit what you save and protect the resulting artifact like the normal state file.

// During the authenticated setup, after login has succeeded:
const sessionStorageByOrigin = await page.evaluate(() => ({
  [location.origin]: Object.fromEntries(
    Array.from({ length: sessionStorage.length }, (_, i) => {
      const key = sessionStorage.key(i);
      return [key, sessionStorage.getItem(key)];
    }),
  ),
}));
await context.storageState({ path: 'playwright/.auth/state.json' });
await import('node:fs/promises').then(({ writeFile }) =>
  writeFile('playwright/.auth/session-storage.json', JSON.stringify(sessionStorageByOrigin), { mode: 0o600 }),
);
// In the capture setup, before navigating to the private page:
import { readFile } from 'node:fs/promises';

const sessionStorageByOrigin = JSON.parse(
  await readFile('playwright/.auth/session-storage.json', 'utf8'),
);
await context.addInitScript((values) => {
  const entries = values[location.origin];
  if (!entries) return;
  for (const [key, value] of Object.entries(entries)) {
    if (value !== null) sessionStorage.setItem(key, value);
  }
}, sessionStorageByOrigin);
const page = await context.newPage();
await page.goto(new URL('/reports/daily', baseURL).toString());

Use the same origin key the application uses, and install the initialization script before navigating. Do not serialize every session value blindly if the origin stores unrelated or short-lived data. Confirm that this method fits the site’s security model and current Playwright version.

6. Schedule refresh and protect the artifact

  • Refresh on evidence of expiry. Detect a redirect to login, a missing signed-in marker, or another site-specific unauthenticated response. Regenerate the state through the setup flow; there is no universal session lifetime.
  • Set an explicit artifact lifetime. Persist state only as long as the workflow needs. If it does not need to survive runs, keep it in a per-run location that is cleaned automatically. Playwright’s guide notes that the test project output directory is cleaned before each run.
  • Restrict every copy. Limit who and what can read the artifact. Avoid broad artifact retention and ensure cleanup also covers failed or cancelled runs.
  • Keep setup and capture separate. This makes authentication refresh distinct from navigation and screenshot work, and lets the capture job fail clearly when the state is missing or expired.
  • Choose accounts for concurrency. A shared account can work when concurrent captures do not conflict with server-side state. Use distinct accounts for parallel workers that modify shared state.
  • Follow the site’s rules. Confirm that unattended session reuse and the account’s concurrent use are permitted. The target site determines MFA, session revocation, and account-sharing behavior.

Playwright explicitly warns that the state file may contain sensitive cookies and headers that could be used to impersonate the account. Keep it out of the repository, and treat copies and backups with the same care.

7. Capture with cURL, Python, or Node.js when browser login is not needed

A screenshot API is a different option from reusing a browser login state. It is suitable only if the target page can be captured through the service’s supported request setup; do not assume a browser storageState file can be uploaded or reused by an API. For pages requiring a private authenticated browser session, use the Playwright workflow above unless the API explicitly supports the authentication mechanism you need.

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 import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its one-call screenshot example is shown below; see the API 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

ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response reports the page verdict and billing status in headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. For a private page, use only an authentication method the service explicitly supports; this example does not transfer Playwright login state.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

8. Troubleshooting scheduled captures

Symptom Likely cause Fix
The capture redirects to the login page The saved session expired, was revoked, or state was saved before login completed. Check the signed-in marker during setup, regenerate the state, and refresh it when the capture detects an unauthenticated page.
It works locally but not in CI The state artifact was not uploaded, downloaded to the expected path, or available to the capture process. Check artifact handoff and working directory. Check file presence without printing contents, and restrict access to the capture job.
The page is signed in locally but not in the job The app may rely on sessionStorage, an unsupported state mechanism, or a different origin. Inspect the site’s auth behavior. Restore required sessionStorage before navigation, and verify origin and installed Playwright version.
Setup saves an unauthenticated state The script saved too early, before the server completed login or set its cookies. Wait for a reliable post-login URL or signed-in UI element before saving.
Parallel captures interfere with one another The account has mutable server-side state or concurrent sessions affect each other. Use separate accounts for workers that modify shared state, or serialize those captures if the site’s rules require it.
A MFA or CAPTCHA step blocks setup The site requires interactive or additional authentication that the unattended flow cannot complete. Use a site-supported automation or service-account flow if available, or arrange an approved manual refresh. Do not bypass the site’s controls.
The auth file appears in a commit or log The artifact was placed in a tracked directory or its contents were printed. Remove it from tracking, rotate or revoke the affected session if exposure is possible, and use a protected location with narrow access.
State works once, then stops The site rotated or expired the session, or invalidated it after a security event. Regenerate state and base refresh timing on observed site behavior or an explicit expiry signal; no universal cadence applies.

9. Performance, reliability, and cost

Reusing state avoids repeating the login flow on every capture, so the scheduled workflow has fewer authentication steps. The main reliability cost is session expiry: make authentication failure explicit, keep refresh separate, and avoid treating a login page as a valid screenshot. UI login can also be sensitive to form changes, MFA, and site-specific redirects.

Protecting and refreshing a state artifact adds operational work. Keep the artifact short-lived where practical, limit copies, and use one account only when concurrency is safe. The research sources provide no universal session duration, refresh interval, performance benchmark, or monetary cost for this workflow; those depend on the site, CI environment, and any services you choose.

10. FAQ

Can I reuse the same state file for every scheduled run?

Yes, while the site accepts that session and the artifact remains protected. Recreate it when the site invalidates it or your policy requires rotation.

Does storageState save passwords?

It saves browser authentication state, not a general-purpose password vault. That state can still grant account access, so handle it as a credential.

Can multiple workers share one state file?

They can if concurrent sessions are allowed and the workers do not conflict over server-side state. Separate accounts are safer for parallel workers that make changes.

How often should I refresh the state?

There is no universal interval. Use the site’s documented session policy or refresh when the job detects that authentication has expired.

Primary references