ScreenshotNeo

BlogHow-to

How to Capture Screenshots of Pages Behind a Login with n8n and a Browser Session

Use n8n to trigger Playwright, restore an authorized browser session, verify the login, and capture a page screenshot. Includes setup, security, and troubleshooting.

By the ScreenshotNeo team4 October 202612 min read

Direct answer: use n8n to orchestrate a browser task and Playwright to render the page. Authenticate with an account authorized to view the target, save the browser context’s authentication state, restore it for each capture, confirm the session is still signed in, then take the screenshot. An n8n HTTP Request node can call your browser worker or an API, but a plain HTTP request does not render a complete interactive browser page. See the official Playwright authentication guide, BrowserContext API, Page API, and n8n HTTP Request node documentation.

This pattern is for pages you are authorized to access. It does not bypass MFA, CAPTCHA, SSO restrictions, or other access controls. The exact login steps and authentication lifetime depend on the target application.

1. Choose how the browser gets authenticated

There are two typical starting points. Prefer the application’s supported authentication API when it exists and is appropriate: it can avoid brittle UI selectors. Otherwise, automate the normal login UI and wait for an explicit success condition, such as a signed-in account element or a known post-login URL. Playwright documents both approaches; neither works identically for every site.

Approach Use when Trade-off
Supported login API The target documents an API login intended for this use. Often less sensitive to layout changes, but the API’s supported behavior and returned state are site-specific.
Browser UI login The normal login page is the supported route. Closer to a real user flow, but selectors, redirects, MFA, and UI changes require maintenance.
Reuse saved state The account can remain signed in across runs. Reduces repeated authentication work, but state expires and must be protected like a credential.

2. Install a browser worker

Run Playwright in an environment that can launch its supported browser. This is a separate browser execution component; the example below exposes a small HTTP endpoint for an n8n HTTP Request node to call. It uses Node.js and Express as the bridge, and Playwright to render and capture. Keep the worker private or protect its endpoint with authentication before connecting it to a workflow.

mkdir n8n-screenshot-worker
cd n8n-screenshot-worker
npm init -y
npm install express playwright
npx playwright install chromium

Create server.mjs. Replace the example login URL, selectors, success URL, and authorized target host with values for your application. The first request performs a UI login and writes storage state; subsequent requests reuse it. This minimal example serializes requests so that simultaneous calls do not write the state file at once.

import express from 'express';
import { chromium } from 'playwright';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';

const app = express();
app.use(express.json({ limit: '32kb' }));
const port = Number(process.env.PORT || 3000);
const authDir = process.env.AUTH_DIR || '/var/lib/n8n-shot/auth';
const authFile = path.join(authDir, 'target.json');
const expectedHost = process.env.TARGET_HOST; // e.g. dashboard.example.com
const workerToken = process.env.WORKER_TOKEN;
let queue = Promise.resolve();

function authorized(req, res, next) {
  if (!workerToken || req.get('authorization') !== `Bearer ${workerToken}`) {
    return res.status(401).json({ error: 'Unauthorized worker request' });
  }
  next();
}

function validTarget(value) {
  let u;
  try { u = new URL(value); } catch { return null; }
  if (u.protocol !== 'https:' || u.hostname !== expectedHost) return null;
  return u;
}

app.post('/capture', authorized, (req, res) => {
  const target = validTarget(req.body?.url);
  if (!target) return res.status(400).json({ error: 'Provide an HTTPS URL on the configured target host' });

  // Keep this sample single-flight to avoid concurrent auth state writes.
  const job = queue.then(async () => {
    let browser;
    try {
      await mkdir(authDir, { recursive: true, mode: 0o700 });
      browser = await chromium.launch({ headless: true });
      const hasState = await readFile(authFile).then(() => true, () => false);
      const context = await browser.newContext(hasState ? { storageState: authFile } : {});
      const page = await context.newPage();

      if (!hasState) {
        await page.goto(process.env.LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 45000 });
        await page.locator(process.env.USERNAME_SELECTOR).fill(process.env.TARGET_USERNAME);
        await page.locator(process.env.PASSWORD_SELECTOR).fill(process.env.TARGET_PASSWORD);
        await page.locator(process.env.SUBMIT_SELECTOR).click();
        await page.waitForURL(process.env.SIGNED_IN_URL, { timeout: 45000 });
        await page.locator(process.env.SIGNED_IN_SELECTOR).waitFor({ state: 'visible', timeout: 15000 });
        await context.storageState({ path: authFile, indexedDB: true });
      }

      await page.goto(target.href, { waitUntil: 'domcontentloaded', timeout: 45000 });
      // Set this to a visible element that only authenticated users can see.
      await page.locator(process.env.SIGNED_IN_SELECTOR).waitFor({ state: 'visible', timeout: 15000 });
      // Replace with the page-specific readiness condition when possible.
      await page.locator(process.env.CAPTURE_READY_SELECTOR || 'body').waitFor({ state: 'visible', timeout: 15000 });
      const png = await page.screenshot({ fullPage: true, animations: 'disabled', type: 'png', timeout: 30000 });
      await context.close();
      res.set('Content-Type', 'image/png').send(png);
    } catch (error) {
      res.status(502).json({ error: 'Capture failed', detail: String(error?.message || error) });
    } finally {
      await browser?.close();
    }
  });
  queue = job.catch(() => {});
});

app.listen(port, () => console.log(`Screenshot worker listening on ${port}`));

Set the environment variables in your process manager or secret store. Do not paste actual credentials into this article’s code, workflow exports, source control, or ordinary execution logs.

PORT=3000
WORKER_TOKEN=replace-with-a-long-random-secret
TARGET_HOST=dashboard.example.com
LOGIN_URL=https://dashboard.example.com/login
SIGNED_IN_URL=https://dashboard.example.com/home
USERNAME_SELECTOR=input[name="email"]
PASSWORD_SELECTOR=input[name="password"]
SUBMIT_SELECTOR=button[type="submit"]
SIGNED_IN_SELECTOR=[data-testid="account-menu"]
CAPTURE_READY_SELECTOR=[data-testid="report-content"]
TARGET_USERNAME=your-authorized-account
TARGET_PASSWORD=your-secret

For a production deployment, configure those values using the hosting platform’s secret mechanism rather than a checked-in .env file. Restrict network access to the worker and persist its auth directory on storage with appropriate access controls. A container with ephemeral storage will lose the saved state when it is replaced.

3. Save a session once, then reuse it

The worker example saves state after it observes a successful UI login. Playwright storage state normally includes cookies and local storage; its API can include IndexedDB when the application keeps authentication data there. Save only after the login has completed and any redirect chain has finished. The example waits for both the expected URL and a signed-in element before writing state.

To bootstrap state manually, the same login portion can be run as a one-off script using the target site’s own login form:

import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';

await mkdir('/var/lib/n8n-shot/auth', { recursive: true, mode: 0o700 });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();

await page.goto('https://dashboard.example.com/login');
await page.locator('input[name="email"]').fill(process.env.TARGET_USERNAME);
await page.locator('input[name="password"]').fill(process.env.TARGET_PASSWORD);
await page.locator('button[type="submit"]').click();
await page.waitForURL('https://dashboard.example.com/home');
await page.locator('[data-testid="account-menu"]').waitFor({ state: 'visible' });
await context.storageState({ path: '/var/lib/n8n-shot/auth/target.json', indexedDB: true });

await browser.close();

Change those example selectors and URLs to match the authorized application. If the target uses session storage, Playwright’s normal storage-state flow does not persist it; Playwright documents a custom save-and-restore approach. Passkeys and device-bound flows can also require a different supported process. Do not assume that copying cookies alone reproduces every login.

4. Connect n8n to the worker

  1. Add a trigger appropriate to the workflow, such as a Schedule Trigger or a Webhook Trigger.
  2. Validate or select a fixed authorized target URL. Avoid accepting arbitrary URLs from untrusted webhook callers; the worker example restricts requests to one HTTPS host to reduce accidental access to unrelated internal or external destinations.
  3. Add an HTTP Request node with method POST, URL https://your-private-worker.example/capture, and JSON body { "url": "https://dashboard.example.com/reports/weekly" }.
  4. Set the Authorization header to Bearer <worker token> using the n8n credential mechanism available in your deployment. The node should receive the worker response as binary image data; configure the node’s response handling for a file/binary response in your n8n version.
  5. Connect the binary result to the destination node you need, such as storage or a notification step. Confirm the destination’s binary property name and file handling in its node settings.
  6. Set workflow error handling so that a failed capture is visible and can be retried where appropriate. Avoid sending authentication state or passwords to error outputs.

Exact node labels can vary by n8n version and deployment. The supported division of work is stable: n8n triggers, passes parameters, routes the result, and manages workflow credentials; the browser worker handles interactive page rendering and screenshot capture.

5. Screenshot settings and readiness checks

Playwright’s page.screenshot() supports a full-page capture and output options. The example uses PNG, full page, and disabled animations. Change fullPage to false for just the current viewport. For JPEG, set type: 'jpeg' and a suitable quality value; quality applies to JPEG. Use path to write directly to a file, or omit it to receive a buffer, as in the worker. The buffer is returned to n8n with an image content type.

Wait for the specific content that makes the image useful. domcontentloaded is a navigation milestone, not proof that an application’s data, charts, or images have finished rendering. Prefer a selector tied to the page’s main content. A fixed delay is simple but can be both wasteful and unreliable. If the application shows a loading indicator, wait for it to disappear or for the finished content to appear. Lazy-loaded content may require scrolling through the page before a full-page screenshot; implement and verify that behavior for the target site.

  • Viewport: pass viewport: { width, height } to browser.newContext() to control the visible page size.
  • Full page: use fullPage: true when the whole document is needed. Very tall pages may consume considerable memory.
  • Element: use page.locator('selector').screenshot() to capture one element after confirming it is visible and stable.
  • Image output: use PNG for lossless output and screenshots with text; JPEG can reduce file size with some quality loss.
  • Wait strategy: use locator waits or a site-specific readiness signal. Avoid treating network idle as universally reliable; applications with ongoing requests may never become idle.
  • Browser context: set locale, timezone, viewport, or other context settings to match the output you need. Keep settings consistent between authentication and capture when the site binds sessions to browser characteristics.

6. Protect the authenticated state

Storage state is credential-equivalent. Playwright warns: “The browser state file may contain sensitive cookies and headers that could be used to impersonate you or your test account.” Store it in a restricted directory, exclude it from source control, limit its retention, and do not expose it in n8n execution data or logs. See the authentication guidance.

n8n workflow sharing affects credential exposure: workflow editors can use credentials that the workflow uses, including credentials not explicitly shared with them. Restrict workflow editing and access to the worker token accordingly; see n8n workflow sharing. If you self-host n8n, review its security audit, which can identify issues involving credentials, file-system access, risky nodes, unprotected webhooks, and instance security.

  • Use a dedicated, least-privilege account for automation where the target supports it.
  • Keep credentials and storage-state files out of Git, shared folders, logs, and workflow output.
  • Limit who can edit the workflow and who can call the browser worker.
  • Use HTTPS between n8n and the worker and validate incoming URL inputs.
  • Set a retention and refresh process; delete or replace expired state and reauthenticate through the approved login flow.

7. Troubleshooting

Symptom Likely cause Fix
The screenshot shows a login page The saved session expired, state was not loaded, or the app stored auth outside the saved state. Check the final URL and signed-in selector before capture. Reauthenticate through the supported flow. Determine whether the site uses IndexedDB or session storage.
Login selector times out The form markup changed, the wrong login page loaded, or an SSO step redirected elsewhere. Inspect the login page and update selectors. Handle documented redirects explicitly. Do not try to bypass access checks.
Login seems successful but state is not signed in Authentication cookies may be set after redirects, or the script saved too early. Wait for the final signed-in URL and an account-only element before saving state.
MFA or CAPTCHA blocks automation The site requires a human or an approved device verification. Use the site’s supported automation access method or an authorized manual session renewal. No universal unattended workaround is established.
HTTP Request node returns an error The worker is unreachable, token is missing or wrong, request body is malformed, or capture returned an error. Check worker reachability from the n8n runtime, authorization header, JSON body, and worker logs. Keep logs free of secrets.
Image is blank or missing dynamic content The script captured before content rendered or selected the wrong readiness marker. Wait for the page-specific content, inspect the final page URL, and use a stable selector. Account for lazy loading if needed.
Full-page image is too large or times out The document is exceptionally long or contains heavy media. Capture an element or viewport, reduce the requested page scope, or use a PDF/page-range workflow if suitable. Set practical timeouts and resource limits.
State file disappears after deployment The worker filesystem is ephemeral or its auth directory changed. Persist the restricted auth directory or reauthenticate as part of deployment operations. Verify file ownership and permissions.
Concurrent jobs produce intermittent auth failures Requests are racing to update shared state or altering shared account data. Serialize state creation, use separate accounts or state files for independent jobs, and avoid shared mutable account actions.

8. Performance, reliability, and cost

Opening Chromium and loading a page are the main per-capture work. Reusing authenticated state avoids repeating the login flow on every run, but it does not remove navigation or rendering costs. A persistent browser process can reduce repeated startup work if your hosting arrangement supports it, though you must isolate contexts between jobs and manage browser lifecycle carefully. The sample starts a browser for each request to keep the example simple.

Make reliability explicit: use a bounded navigation timeout, wait for a page-specific signal, verify the authenticated view, return a clear error on failure, and configure n8n to notify or retry transient failures. Reauthenticate expired state rather than retrying the same invalid file indefinitely. Keep parallelism within the worker’s available CPU and memory, especially for full-page captures. Avoid retries for permanent errors such as invalid credentials or denied access.

There is no universal per-screenshot runtime or cost figure: both depend on the target site, browser hosting, image dimensions, frequency, and execution environment. Budget for browser compute, storage, and any n8n hosting limits. Full-page captures and concurrent browser contexts use more resources than viewport captures. Reduce work by capturing only the required page region, setting sensible timeouts, and avoiding unnecessary repeated authentication.

Or skip the browser setup

If the page is publicly accessible, ScreenshotNeo can return an image or PDF from one GET request. It is a website screenshot API and MCP server for developers. It does not accept or restore a private logged-in browser session, so use the Playwright workflow above for login-protected content.

For a public page, see the ScreenshotNeo API documentation and call it like this:

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}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Can n8n take the screenshot by itself?

The workflow needs a browser-rendering component for an interactive page. n8n can coordinate the request and handle the result; its HTTP Request node makes HTTP calls but does not itself render a complete browser page.

Can I send cookies directly to the worker?

You can configure a browser context with supported storage state, but treat it as sensitive credential material. Reusing a protected state file is easier to manage than putting cookie values in workflow fields or logs.

Will saved state work forever?

No. The target site controls session lifetime and may invalidate sessions. Detect the signed-out state and renew authentication through an authorized flow.

Does this work for every SSO or MFA setup?

No. The authentication flow is application-specific. Use only supported access methods; some device-bound or interactive checks may prevent unattended capture.

Can ScreenshotNeo capture a page that requires my login?

The ScreenshotNeo API example is for a public URL. For private logged-in pages, use an authorized browser session with the method described above.

References