ScreenshotNeo

BlogHow-to

How to Capture Web Screenshots from a Site That Requires a Login Form POST

Log in with Playwright, verify the authenticated page, and capture it. Includes browser and form-POST workflows, session reuse, troubleshooting, and a ScreenshotNeo option.

By the ScreenshotNeo team4 October 20269 min read

To screenshot a page that requires a login form POST, first authenticate, confirm that the site has established a session, then navigate to the protected page and capture it. With a conventional interactive login, Playwright can fill and submit the form. If the site explicitly supports direct API authentication, Playwright can POST form data and reuse the resulting cookies. In either case, verify an authenticated page signal before taking the screenshot: a saved image alone does not show that login succeeded.

This guide uses Playwright with Node.js. It covers both approaches, session reuse, troubleshooting, and a managed screenshot option. See the official Playwright authentication guide, APIRequestContext reference, and Page screenshot reference.

1. Install Playwright

Start a Node.js project and install Playwright. The commands below install the package and its Chromium browser:

npm init -y
npm install playwright
npx playwright install chromium

Set credentials through environment variables rather than placing them in source code. For example, in a Unix-like shell:

export LOGIN_USER='your-username'
export LOGIN_PASSWORD='your-password'

Use the equivalent environment-variable setup for your shell or CI provider. Do not commit credentials or authenticated browser state to source control.

2. Log in through the browser form

For an ordinary interactive login form, automate the same browser interaction a user performs: open the login page, fill the fields, submit, and wait for evidence that authentication completed. Replace the example URLs and locators with the site’s actual login form, submit control, and a page element that only appears after a successful login.

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

const loginUrl = 'https://example.com/login';
const protectedUrl = 'https://example.com/account';
const username = process.env.LOGIN_USER;
const password = process.env.LOGIN_PASSWORD;

if (!username || !password) {
  throw new Error('Set LOGIN_USER and LOGIN_PASSWORD before running this script.');
}

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  viewport: { width: 1440, height: 1000 },
});
const page = await context.newPage();

try {
  await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });

  // Prefer accessible labels when the site provides them.
  await page.getByLabel('Email').fill(username);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // A redirect may be part of the authentication flow. Wait for a URL
  // or a UI element that proves the session is authenticated.
  await page.waitForURL(url => !url.pathname.endsWith('/login'), {
    timeout: 30_000,
  });
  await page.getByRole('link', { name: 'Account' }).waitFor({
    state: 'visible',
    timeout: 15_000,
  });

  await page.goto(protectedUrl, { waitUntil: 'domcontentloaded' });
  await page.getByRole('heading', { name: 'Account overview' }).waitFor({
    state: 'visible',
    timeout: 15_000,
  });

  await page.screenshot({
    path: 'account.png',
    fullPage: true,
    animations: 'disabled',
  });
} finally {
  await browser.close();
}

Save this as login-and-capture.mjs and run node login-and-capture.mjs. The labels, button name, URL condition, and heading are examples; use selectors and success signals that match the target site. If a site has no accessible labels, use stable selectors such as documented test IDs rather than brittle positional selectors.

Choose a useful success signal

  • Final URL: wait for the known post-login route if the site redirects reliably.
  • Authenticated UI: wait for an account menu, signed-in navigation, or other element that unauthenticated visitors cannot see.
  • Protected content: after navigating to the target, wait for a distinctive heading or page element before capturing.

Login cookies may be set across multiple redirects. Waiting only for the submit click or a network response can capture the login screen, an intermediate redirect, or an error page. A site may also report a successful login while denying access to a particular protected resource, so check the target page itself.

3. Use a direct form POST only when the site supports it

If the application documents an authentication endpoint intended for direct API login, you can submit form-encoded data with Playwright’s API request context. The context handles cookies and redirects; its cookies can then be used by a browser context. Do not guess an endpoint or assume that a visible browser form is equivalent to a simple POST. Hidden CSRF fields, JavaScript-generated values, multifactor authentication, bot checks, and other server-side requirements can make direct login fail.

// api-login-and-capture.mjs
import { chromium, request } from 'playwright';

const authUrl = 'https://example.com/session'; // Use the documented endpoint.
const protectedUrl = 'https://example.com/account';
const username = process.env.LOGIN_USER;
const password = process.env.LOGIN_PASSWORD;

if (!username || !password) {
  throw new Error('Set LOGIN_USER and LOGIN_PASSWORD before running this script.');
}

const api = await request.newContext();
const browser = await chromium.launch({ headless: true });

try {
  const response = await api.post(authUrl, {
    form: {
      username,
      password,
    },
  });

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

  // Export cookies from the API context and install them in the browser.
  const storage = await api.storageState();
  const context = await browser.newContext({ storageState: storage });
  const page = await context.newPage();

  await page.goto(protectedUrl, { waitUntil: 'domcontentloaded' });
  await page.getByRole('heading', { name: 'Account overview' }).waitFor({
    state: 'visible',
    timeout: 15_000,
  });
  await page.screenshot({ path: 'account.png', fullPage: true });

  await context.close();
} finally {
  await api.dispose();
  await browser.close();
}

Save as api-login-and-capture.mjs and run it with the same environment variables. Change the endpoint and form field names to the application’s documented API contract. Some APIs return tokens instead of setting cookies; in that case, follow the application’s documented browser authentication mechanism rather than assuming the example transfers the token into a usable session.

4. Reuse an authenticated session when needed

For repeated captures, Playwright can save browser storage state after login and load it into a later context. This can avoid submitting the form for every page, but sessions expire and the saved state is sensitive.

// After successful UI login, before closing the context:
await context.storageState({ path: '.auth/state.json' });

// In a later run:
const context = await browser.newContext({
  storageState: '.auth/state.json',
});

Keep the state file out of version control, restrict access to it, and refresh it through the site’s supported login flow when it expires. It may contain cookies or other material that can impersonate the account. Playwright recommends keeping authentication state in a private directory such as .auth and excluding that directory from source control. If the application stores authentication only in browser session storage, verify that your chosen state-reuse approach preserves what the app needs.

5. Capture settings and page readiness

Playwright’s screenshot API can save to a path or return image bytes. For this task, the most useful options are:

Option What it does When to use it
path Saves the screenshot to a file; the extension determines the image format. Use a path such as account.png for a local artifact.
fullPage Captures the full scrollable page rather than only the viewport. Use for long account pages; check whether lazy-loaded content appears after scrolling.
type Selects png or jpeg when returning bytes or when the extension does not decide the format. PNG preserves sharp interface details; JPEG can produce smaller files.
quality Sets JPEG quality. Applies to JPEG screenshots, not PNG.
scale Uses CSS pixels or device pixels for output sizing. Use css for smaller output or device for device-scale output.
animations Disables or completes animations during capture. Use disabled for more stable output.
mask Overlays selected locators in the screenshot. Use when a screenshot must obscure sensitive or changing page regions.
timeout Limits how long screenshot preparation can take. Increase it for unusually large or slow pages, while still handling timeouts.

Choose a readiness condition tied to the content you need. domcontentloaded waits for the document to be parsed but does not guarantee that client-rendered data, images, or charts are ready. A locator wait is often a better signal for a specific page. Use a deliberate delay only when the application provides no stronger signal; fixed sleeps make captures slower and can still be too short.

6. Troubleshooting

Symptom Likely cause Fix
Screenshot shows the login form Credentials were rejected, the submit selector missed, or the script continued before redirects finished. Check the form locators and credentials; wait for a post-login URL and an authenticated-only element.
Login succeeds but target redirects to login The session cookie was not set, was not transferred to the browser, expired, or has a domain/path restriction. Use the same browser context for UI login and capture. For API login, confirm the documented endpoint sets cookies compatible with the target origin.
API POST returns an error or HTML login page The endpoint, field names, CSRF token, or content type is wrong, or the site requires interactive behavior. Use the documented API contract. If there is no supported direct route, automate the browser form instead.
Script times out waiting for URL or selector The success condition does not match the site’s actual flow, or login needs an extra step. Inspect the final URL and page state; adjust the condition to a stable authenticated signal. Handle MFA or challenge steps according to the site’s supported process.
Screenshot is blank or incomplete The page is still rendering, client-side requests have not finished, or content loads on scroll. Wait for the relevant heading or content, scroll if needed for lazy content, and capture after it appears.
Works locally but fails in CI Different environment variables, browser dependencies, viewport, or network access. Confirm secrets are configured in CI, install the matching Playwright browser, and log the observed URL and non-sensitive page state for diagnosis.
Saved state works once, then fails The session expired or the application rotated/revoked its cookies. Reauthenticate and save fresh state. Treat state files as credentials and avoid sharing them.

7. Performance, reliability, and cost

Browser startup and login add overhead. Reusing a browser process and authenticated context for multiple captures can reduce repeated setup, but keep each account’s session isolated and avoid sharing mutable contexts across unrelated jobs. Use the narrowest screenshot needed: viewport captures are generally smaller and quicker than full-page captures. Wait for page-specific content rather than an unnecessarily broad network-idle condition, since persistent connections can prevent network idle from occurring.

For reliability, make authentication failure explicit: check the final URL, a signed-in UI signal, and the protected page’s expected content. Record status and diagnostic information without logging passwords, cookies, authorization headers, or storage-state contents. Retry only transient navigation or service failures; repeated credential submissions can trigger lockouts or security controls. There is no universal runtime or cost figure here: it depends on the target site, browser environment, network, and how often you capture.

Or skip the browser setup

If the protected page can be accessed using a cookie or Authorization header, ScreenshotNeo can capture it with one GET request. It does not submit login forms or bypass authentication challenges: obtain valid session credentials through the site’s supported login flow, then supply the appropriate cookie or authorization header using the API’s documented options. See the ScreenshotNeo API documentation for authentication headers, cookies, and request parameters.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/account \
  -H 'Cookie: session=YOUR_SESSION_COOKIE' \
  -o account.webp

Keep the session value private and use the site’s intended access controls. The equivalent Python request is:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://example.com/account",
    },
    headers={"Cookie": "session=YOUR_SESSION_COOKIE"},
    timeout=90,
)
r.raise_for_status()
open("account.webp", "wb").write(r.content)

And in Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/account',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`, {
  headers: { Cookie: 'session=YOUR_SESSION_COOKIE' },
});
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('account.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers indicate the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDFs. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Can I take a screenshot by sending the login form POST alone?

Not by itself. The POST must establish a session that the page renderer can use, and the capture must happen after the protected page has loaded and authenticated content is confirmed. A direct POST is suitable only when the site supports that authentication route.

Should I use the UI flow or the API flow?

Use browser form submission when the site’s endpoint behavior is unclear or the login depends on browser interactions. Use direct form POST when the application documents a compatible API login and its resulting session can be used by the browser.

Does a screenshot prove the account has access?

No. Verify both an authenticated signal and the expected content on the target page before saving the image.

Can I reuse the session for several protected URLs?

Yes, while it remains valid. Reuse the authenticated context or saved storage state, keep it private, and handle expiration by authenticating again.