ScreenshotNeo

BlogHow-to

How to Automate Login Before a Scheduled Website Screenshot Without Saving Passwords

Use Playwright to log in once, save protected browser state, and reuse it for scheduled screenshots without putting a password in the capture script.

By the ScreenshotNeo team4 October 20269 min read

The practical pattern is to authenticate once with Playwright, save the resulting browser state, and load that state in each scheduled screenshot run. The recurring capture script does not need a plaintext password, but the saved state is still a credential: it can contain cookies and headers that let someone impersonate the account. Keep it secret, keep it out of source control, and detect expired sessions instead of saving a screenshot of the login page.

This guide uses Playwright for a site where automated access is permitted. Its authentication guide supports both browser-driven login and API-based authentication where the application provides a suitable endpoint. Its Page screenshot API captures the authenticated page.

1. Choose how to authenticate

Method Use it when Trade-off
Browser UI You need to follow the site’s normal login flow, including redirects or UI-specific steps. More dependent on selectors, page changes, and identity-provider behavior.
Supported application API The site offers an appropriate authentication API and you are authorized to use it. Usually simpler to automate, but the endpoint and response behavior are application-specific.
Re-authenticate on every run The session cannot be safely or reliably reused. Requires a protected credential source at run time and may encounter MFA or identity-provider changes.

Do not assume unattended login is possible for every account. MFA, passkeys, bot defenses, federated login, policy restrictions, and session expiration can change the implementation or make scheduled capture unsuitable. Use a dedicated, appropriately limited account where practical.

2. Install Playwright and prepare a state directory

For a small standalone Node.js script, install Playwright and its Chromium browser:

npm init -y
npm install playwright
npx playwright install chromium

Create a directory for the state file and ignore it in Git. Playwright specifically recommends keeping generated authentication state out of repositories because it may permit account impersonation.

mkdir -p playwright/.auth
printf '\nplaywright/.auth/\n' >> .gitignore

Do not put the password in this source file. Supply it only to the one-time login process from a protected environment-specific secret mechanism. Do not echo it, log it, or pass it on a command line that other local processes can inspect. The scheduler, secret store, file permissions, encryption, and retention policy depend on where the job runs; configure those in that environment.

3. Run a one-time login and save browser state

The following example performs a UI login. Replace the example URLs and selectors with stable selectors for the target site. It waits for a signed-in signal before saving state, so a failed login does not create a misleading state file.

// save-auth.mjs
import { chromium } from 'playwright';

const loginUrl = process.env.LOGIN_URL;
const username = process.env.SCREENSHOT_USERNAME;
const password = process.env.SCREENSHOT_PASSWORD;
const authFile = 'playwright/.auth/account.json';

if (!loginUrl || !username || !password) {
  throw new Error('Set LOGIN_URL, SCREENSHOT_USERNAME, and SCREENSHOT_PASSWORD');
}

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();

try {
  await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
  await page.getByLabel('Email').fill(username);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Replace with a stable destination or signed-in element for this site.
  await page.getByRole('link', { name: 'Account' }).waitFor({ state: 'visible', timeout: 30000 });

  await context.storageState({ path: authFile });
  console.log(`Saved authenticated state to ${authFile}`);
} finally {
  await browser.close();
}

Run this setup when the session is first needed and again when it expires. A successful button click alone is not proof of login. Prefer waiting for a known signed-in element or the expected post-login URL. If login requires multiple steps, add the site-specific steps and verify each transition. Do not bypass MFA or bot checks; use an approved authentication path or stop the unattended job.

When the application supports an authentication API

Playwright can also make an API request and save the resulting state. The endpoint, payload, and success checks below are deliberately placeholders: use the site’s documented API and verify its response and resulting session.

// save-auth-api.mjs
import { request } from 'playwright';

const authFile = 'playwright/.auth/account.json';
const api = await request.newContext();

try {
  const response = await api.post(process.env.AUTH_ENDPOINT, {
    data: {
      username: process.env.SCREENSHOT_USERNAME,
      password: process.env.SCREENSHOT_PASSWORD,
    },
  });

  if (!response.ok()) {
    throw new Error(`Authentication failed with HTTP ${response.status()}`);
  }

  await api.storageState({ path: authFile });
} finally {
  await api.dispose();
}

Use API login only when it is supported and appropriate for the application. Some authentication flows require browser navigation or state that the API endpoint does not establish.

4. Load the saved state and capture on each scheduled run

This script uses the saved state to create an authenticated browser context, opens the target page, checks that it is still signed in, and writes a PNG. The scheduler only needs to invoke this script at the desired time. This guide does not prescribe cron syntax or provider-specific job configuration because the runtime is unspecified.

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

const targetUrl = process.env.TARGET_URL;
const authFile = 'playwright/.auth/account.json';
const outputFile = process.env.OUTPUT_FILE ?? 'artifacts/page.png';

if (!targetUrl) throw new Error('Set TARGET_URL');

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ storageState: authFile });
const page = await context.newPage();

try {
  await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });

  // Replace these checks with signals specific to the target site.
  const loginFormVisible = await page.getByLabel('Password').isVisible().catch(() => false);
  const signedIn = await page.getByRole('link', { name: 'Account' }).isVisible().catch(() => false);
  if (loginFormVisible || !signedIn) {
    throw new Error('Authentication state is missing or expired; refresh it and do not publish this capture');
  }

  await page.screenshot({ path: outputFile, fullPage: true, type: 'png' });
} finally {
  await context.close();
  await browser.close();
}

Prepare the output directory before running if it does not exist. Store the screenshot according to the sensitivity of the page: authenticated content may contain private data even when the browser state is protected.

5. Pick screenshot options deliberately

Need Playwright option or pattern Notes
Visible viewport only page.screenshot({ path: 'page.png' }) Default capture is the current viewport.
Entire page fullPage: true Captures the full scrollable page; long pages can create large files.
JPEG or WebP type: 'jpeg' or type: 'webp' Choose a format supported by the installed browser version; set quality where supported for lossy formats.
Higher resolution scale: 'css' or scale: 'device' Device scale can increase dimensions and file size.
Hide or redact a page element mask: [page.locator('...')] Only masks the selected elements. Verify the locator matches the sensitive content before relying on it.
Control dynamic content appearance style: '…' A stylesheet can hide or restyle selected elements during capture; it does not change the site’s saved content.

For example, mask a known account identifier by locator:

await page.screenshot({
  path: 'artifacts/page.png',
  fullPage: true,
  mask: [page.locator('[data-private-account-id]')],
});

Wait for a meaningful page-ready condition before capturing. domcontentloaded avoids waiting for every resource, but it does not guarantee that a client-rendered dashboard, chart, or lazy image is ready. Add a wait for a stable element or application-specific loaded signal. Use a bounded timeout so a broken page does not leave a job hanging indefinitely.

6. Schedule and secure the workflow

  1. Choose a scheduler or CI runner that can launch Node.js and the installed browser. Configure its timezone and run cadence in that environment.
  2. Provide access to the state file only to the job identity. Protect the initial password through the environment’s secret mechanism only during state creation or refresh.
  3. Run capture.mjs and treat a nonzero exit as a failed capture.
  4. Retain or publish the image only as long as the page’s sensitivity allows. Avoid logging page contents, cookies, authorization headers, or secrets.
  5. Monitor for expiration. When the job detects the login page or misses its signed-in signal, fail visibly and refresh the state through the approved login flow.

The saved state may include cookies, local storage, IndexedDB, origin private file system state, and virtual WebAuthn credentials, depending on the options and site. Session storage is a separate edge case: Playwright does not automatically persist it through ordinary storage-state snapshots. If the site relies on session storage, consult the Playwright authentication guide’s documented save-and-restore pattern and scope any injected data to the correct origin.

7. Troubleshooting

Symptom Likely cause Fix
The scheduled capture shows the login form The state expired, login did not complete, or the site uses state not present in the snapshot. Fail the capture on the signed-in check, rerun the login setup, and confirm the site’s auth mechanism. Investigate session storage if relevant.
ENOENT for the state file The job runs from a different working directory or the file was not deployed to the runner. Use a known absolute path or set the working directory explicitly; securely provision the state file to that runner.
Login selector times out The page changed, a redirect is still in progress, or the selector is ambiguous. Inspect the approved login flow, use accessible labels or stable attributes, and wait for the correct page before interacting.
API login returns success but browser is signed out The response did not establish browser-compatible state, or additional origin state is required. Use the documented app flow and validate the resulting browser context before saving it.
Screenshot is blank or incomplete Capture occurred before client rendering, fonts, images, or asynchronous data completed. Wait for an application-specific ready indicator; use bounded waits rather than arbitrary long sleeps.
Browser will not launch on the runner The browser binary or required runtime dependencies are unavailable. Install the Playwright browser for the job environment and follow the host’s supported browser dependency setup.
State file appears in Git changes The ignore rule was added after tracking, or the path differs. Remove it from the index and verify the ignore rule. Rotate/revoke the session if the file was exposed.
Screenshot leaks information despite masking The selector did not match all sensitive regions or content appeared elsewhere. Validate matched elements and inspect output access and retention; masking is not a general-purpose privacy guarantee.

8. Performance, reliability, and cost

Reusing a valid browser state avoids repeating the login interaction during every capture, but each scheduled run still starts a browser, loads the page, and transfers its resources. Full-page captures and high device scale can increase memory, processing time, and output size. Keep waits tied to the page’s actual readiness signal, bound navigation and job durations, and avoid parallel capture volume that the site or runner cannot handle.

Reliability depends on session lifetime, site changes, network availability, and the scheduler. A useful job should exit unsuccessfully if authentication or page readiness fails, preserve enough non-sensitive diagnostics to identify the stage, and avoid treating an error page as a valid screenshot. The cost of this DIY approach depends on the browser host and scheduler; no provider or rate is assumed here.

Or skip the browser setup

If the page is publicly reachable and does not require your authenticated session, ScreenshotNeo can capture it with one GET request. It is a website screenshot API and MCP server from Yorker Media. See the API documentation for parameters and response details.

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 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. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. It does not replace the authenticated Playwright workflow for pages that require your signed-in browser session.

Start free with 1,000 screenshots a month and no card.

FAQ

Does this avoid storing any credential?

No. It avoids putting the recurring password in the capture script, but the saved browser state is sensitive and can enable account access.

Can I safely reuse one state file forever?

No. Sessions can expire or be revoked. Detect that condition and regenerate state through the site’s allowed login flow.

Can ScreenshotNeo capture a private page using this saved Playwright state?

This article’s ScreenshotNeo example is a URL-based API request, not a transfer of Playwright’s authenticated browser state. Use Playwright for a page that depends on that session.

Should I capture a full page every time?

Only when the whole document is needed. A viewport capture is usually smaller and avoids including unrelated lower-page content.