ScreenshotNeo

BlogHow-to

How to Capture Scheduled Website Screenshots with Authenticated Session Cookies

Use Playwright storage state to capture recurring screenshots of pages behind sign-in, then schedule the job securely and detect expired sessions.

By the ScreenshotNeo team4 October 20269 min read

To capture a scheduled screenshot of a page that requires sign-in, authenticate once with Playwright, save the browser’s authenticated storage state securely, then load it in a scheduled run. Navigate to the page, verify that the session is still authenticated, and capture the screenshot. Treat the saved state as a credential: anyone who can use its cookies or headers may be able to impersonate that account.

This guide uses Node.js, Playwright, and GitHub Actions as a concrete example. The exact storage and refresh steps depend on the target application; no particular login flow or site behavior is assumed.

1. Install Playwright and prepare the project

Use a supported Node.js environment, then install Playwright and its browser. Keep generated authentication files in a private, ignored directory.

npm init -y
npm install --save-dev playwright
npx playwright install chromium
mkdir -p .auth screenshots
printf '\n.auth/\nscreenshots/\n' >> .gitignore

For repeatable automation, pin the dependency version in your lockfile and install from that lockfile in CI. Browser and Playwright versions should be kept compatible by installing the browser through Playwright’s installer.

2. Sign in and save authenticated browser state

For a UI login, create a clean browser context, sign in through the site’s supported flow, wait for a reliable authenticated condition, and save storage state. The example expects credentials in environment variables and an authenticated page element whose selector you replace with one specific to your application.

// save-auth.js
const { chromium } = require('playwright');

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

  try {
    await page.goto(process.env.LOGIN_URL, { waitUntil: 'domcontentloaded' });
    await page.locator(process.env.USERNAME_SELECTOR).fill(process.env.SITE_USERNAME);
    await page.locator(process.env.PASSWORD_SELECTOR).fill(process.env.SITE_PASSWORD);
    await page.locator(process.env.SUBMIT_SELECTOR).click();

    // Replace with a stable post-login signal for your application.
    await page.locator(process.env.AUTHENTICATED_SELECTOR).waitFor({ state: 'visible', timeout: 30000 });
    await context.storageState({ path: '.auth/state.json', indexedDB: true });
  } finally {
    await browser.close();
  }
})();

Run it locally with the required environment variables set. Do not place real passwords directly in the source file or commit .auth/state.json. The saved file can contain session cookies, local storage, IndexedDB data, and other authentication material.

For sites that support an authentication API, Playwright can also set up state through an API request and save the resulting browser context state. Follow the application’s supported authentication mechanism and confirm that the resulting state works in a browser context before relying on it in a scheduled job.

3. Load the state, verify sign-in, and capture a screenshot

The scheduled process should fail clearly if the state is missing or expired. Check an authenticated condition before capture; otherwise, the job can silently save a screenshot of the login page.

// capture.js
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({ storageState: '.auth/state.json' });
  const page = await context.newPage();

  try {
    await page.goto(process.env.TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });

    // Replace with an element present only for a signed-in user.
    await page.locator(process.env.AUTHENTICATED_SELECTOR).waitFor({ state: 'visible', timeout: 20000 });

    // Capture the visible viewport. Use fullPage: true for the complete document.
    await page.screenshot({ path: 'screenshots/capture.png', fullPage: false });
  } finally {
    await browser.close();
  }
})();

To capture the complete document, set fullPage: true. For a specific region, use Playwright’s screenshot clipping options; for an individual element, call the locator’s screenshot method. Choose PNG for lossless output or use the documented screenshot format and quality options when a smaller JPEG is appropriate. Confirm that the application’s authenticated state is still valid before saving or publishing the resulting image.

4. Schedule the capture in GitHub Actions

Create .github/workflows/screenshot.yml. Store credentials as repository or environment secrets, and configure the workflow to run the authentication setup when needed. This example saves storage state during the run, captures once, and uploads only the image artifact.

name: Scheduled screenshot

on:
  schedule:
    - cron: '17 */6 * * *'
  workflow_dispatch:

permissions:
  contents: read

jobs:
  capture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium

      # This step re-authenticates and writes .auth/state.json for this run.
      - name: Save authenticated state
        run: node save-auth.js
        env:
          LOGIN_URL: ${{ secrets.LOGIN_URL }}
          SITE_USERNAME: ${{ secrets.SITE_USERNAME }}
          SITE_PASSWORD: ${{ secrets.SITE_PASSWORD }}
          USERNAME_SELECTOR: ${{ vars.USERNAME_SELECTOR }}
          PASSWORD_SELECTOR: ${{ vars.PASSWORD_SELECTOR }}
          SUBMIT_SELECTOR: ${{ vars.SUBMIT_SELECTOR }}
          AUTHENTICATED_SELECTOR: ${{ vars.AUTHENTICATED_SELECTOR }}

      - name: Capture page
        run: node capture.js
        env:
          TARGET_URL: ${{ vars.TARGET_URL }}
          AUTHENTICATED_SELECTOR: ${{ vars.AUTHENTICATED_SELECTOR }}

      - uses: actions/upload-artifact@v4
        with:
          name: scheduled-screenshot
          path: screenshots/capture.png
          retention-days: 7

The workflow cron is POSIX-style and runs on the repository’s default branch. GitHub documents that scheduled runs use UTC by default and can be delayed or dropped during periods of high load; a scheduled trigger is not a precise-time guarantee. Public-repository schedules can also be disabled after 60 days without repository activity. If missed or late captures matter, monitor successful runs and select an execution service with timing and retry guarantees appropriate to the requirement. See GitHub’s schedule event documentation.

5. Handle existing sessions and sessionStorage

You do not have to repeat the UI login on every scheduled run if you can securely provide a still-valid state file. Save the state in a protected secret store or private runtime location, load it into the context, and refresh it through the supported sign-in flow when it expires. Avoid putting a long-lived state file in a broadly accessible artifact.

Playwright’s storage-state support covers cookies and local storage, and can include IndexedDB when requested. It does not automatically persist sessionStorage. If the application depends on session storage, use a separate, application-specific save and restore mechanism and protect that data with the same care as cookies. Read the official Playwright authentication guide for storage-state details and examples.

6. Capture options and scheduling choices

Need Approach What to check
Whole page fullPage: true Long pages may take longer and produce large files; verify lazy-loaded content is present.
Visible viewport Default page screenshot Set the viewport when creating the context for consistent dimensions.
One component Screenshot a locator Wait for the element to be visible and stable before capture.
Existing login state newContext({ storageState: '…' }) Protect the file, confirm validity, and refresh on expiry.
Session-based state Application-specific sessionStorage restore Standard storage state does not preserve sessionStorage automatically.
Exact execution time Runner with suitable timing guarantees and monitoring GitHub scheduled workflows can be late or dropped under load.
Repeated history Upload or store each dated output privately Set access controls and a retention period for images and logs.

Use a timezone-aware schedule design. GitHub cron schedules are expressed in UTC unless a timezone feature is specified in the current workflow syntax; check the official workflow documentation for supported syntax and daylight-saving expectations. Make outputs unique by run date if you need a history rather than overwriting the same file.

7. Protect credentials, screenshots, and CI artifacts

  • Keep storage-state files out of source control, console output, and broadly shared artifacts.
  • Grant access only to the job and people who need the credential or screenshot.
  • Set artifact retention to the shortest period that supports the workflow.
  • Review traces, reports, and logs before uploading them: they may expose tokens, credentials, test code, or application source.
  • Use a dedicated account with only the access needed for the captured page where the application supports it.
  • Emit a clear failure when the login signal is absent; do not treat a successful browser launch as a successful authenticated capture.

Playwright’s CI guidance warns that reports, traces, and logs can contain sensitive information. Apply the same access and retention controls to screenshots if they show private account data.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One request returns an image or PDF; for a scheduled job, call it from your existing scheduler. This API call captures a public page:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for request options. For a private page, the API accepts custom cookies, headers, and Authorization; use the documented parameters and store those credentials in your scheduler’s secret store. Do not put secret values in source code or logs.

  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, and failed loads are never billed; cache hits are also free, and response headers report the page verdict and billing status.
  • An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.
  • 1,000 screenshots a month are free with no card. Paid plans start at $5 for 3,000 shots; all features are on every plan.

Sign up for 1,000 free screenshots a month, with no card required.

Performance, reliability, and cost

A browser-based job pays the operational cost of launching a browser, loading the page, and storing the output. Full-page captures and pages with many resources can take longer and create larger files. Keep the viewport and wait conditions purposeful, and avoid waiting for network idle when a page’s connections never become idle. A specific authenticated element is usually a more useful readiness signal than an arbitrary delay.

Reliability depends on the site’s session lifetime, login flow, network, browser version, and scheduler. Add a failure signal for a login redirect or missing signed-in element; alert on repeated failures and provide a way to rerun or refresh state. GitHub schedule events are best effort, so account for late or missed invocations where the capture has a deadline.

Direct browser captures have no per-screenshot API price in this example, but require maintaining the runner, browser dependencies, authentication state, artifact storage, and monitoring. ScreenshotNeo’s free tier includes 1,000 shots per month; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. Only clean shots are billed according to the product’s page-verdict behavior.

Troubleshooting

Symptom Likely cause Fix
Screenshot shows the login page State expired, wrong target, or authentication check missing Check the final URL and signed-in selector; refresh state through the supported login flow.
Login succeeds locally but fails in CI Missing secret or variable, changed login behavior, MFA, or environment restrictions Confirm variable names and CI secret scope; use the site’s supported auth route and inspect a restricted failure log without printing credentials.
Cookies load but the app still treats the browser as signed out Authentication also depends on local storage, IndexedDB, or another browser mechanism Save the full supported storage state and verify the site’s actual requirements.
Works until the tab or run ends The site relies on sessionStorage, which standard storage-state persistence does not restore Implement a separately protected sessionStorage save/restore flow if the site supports it.
Timeout waiting for the authenticated element Selector changed, app is slow, or the session is invalid Validate the selector and wait condition; distinguish a slow page from a login redirect and fail with a useful message.
Screenshot is incomplete or content is missing Capture ran before content rendered or lazy content loaded Wait for a meaningful element or app-ready signal; use full-page mode only when the full document is needed.
Scheduled capture did not run at the expected minute Scheduled workflow delays or load-related drops Use workflow monitoring and retries, or a runner with appropriate execution guarantees.
Artifact or trace exposes private data Overly broad access or retention; logs may include sensitive application details Restrict artifact permissions, shorten retention, and inspect files before upload.

FAQ

Only if the target application’s authentication truly depends on that cookie alone. Saved browser state is safer as a working model because applications may also rely on other browser storage.

Will scheduled screenshots happen at an exact time?

Not with GitHub scheduled workflows as a guarantee. Its documentation describes possible delays and dropped runs during high load.

Should I upload the browser state as a CI artifact?

Usually avoid it. The state can enable account impersonation. If persistence across runs requires it, store it in a restricted secret system with deliberate access and rotation.

Does this work for every login flow?

No. MFA, CAPTCHA, federated login, device checks, and application-specific storage can change the setup. Use the site’s supported authentication process and validate it for the actual target.