How to Capture a Webpage Screenshot with an AI Agent Using Authenticated Cookies
Use Playwright to authenticate an isolated browser, verify the signed-in page, and save a screenshot. Includes storage-state handling, options, and troubleshooting.
Use an authorized account in an isolated Playwright browser context, load or establish its authentication state, verify that the intended signed-in page has loaded, then save the rendered page with page.screenshot(). Playwright storage state can include cookies and other browser state, so treat it as a credential. For a reusable state file, keep it out of source control and restrict access.
This guide uses Playwright with Node.js. It covers both saving a state after a normal sign-in and reusing that state in an AI-agent workflow. The AI agent should decide what page to visit and whether the page is correct; keep credential handling and browser actions scoped to the authorized task.
1. Install Playwright
Start a Node.js project and install Playwright and its Chromium browser:
npm init -y
npm install playwright
npx playwright install chromium
Run the examples from the project directory. They use environment variables for the target URL and credentials so secrets do not need to be embedded in the source code.
2. Sign in and save browser state
Use the site’s regular sign-in flow in a dedicated browser context. Wait for a reliable signal that login succeeded before saving state. A URL change alone may not be sufficient if the application redirects through multiple pages; prefer a signed-in page heading, account menu, or another element that is specific to the authenticated view.
Create save-auth.mjs:
import { chromium } from 'playwright';
const loginUrl = process.env.LOGIN_URL;
const username = process.env.APP_USERNAME;
const password = process.env.APP_PASSWORD;
if (!loginUrl || !username || !password) {
throw new Error('Set LOGIN_URL, APP_USERNAME, and APP_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: 60_000 });
await page.getByLabel(/email|username/i).fill(username);
await page.getByLabel(/password/i).fill(password);
await page.getByRole('button', { name: /sign in|log in/i }).click();
// Replace this with a stable, site-specific authenticated-page signal.
await page.getByRole('navigation').getByText(/account|dashboard/i).waitFor({
state: 'visible',
timeout: 30_000,
});
await context.storageState({ path: 'playwright/.auth/user.json', indexedDB: true });
console.log('Saved authenticated browser state.');
} finally {
await browser.close();
}
Adjust the form locators and success signal to match the site. If the site uses a one-time code, passkey, or another interactive challenge, complete its normal authorized sign-in flow; do not try to bypass the site’s access controls. The success signal must only occur after authentication is complete.
Create the directory and exclude its contents from Git:
mkdir -p playwright/.auth
echo 'playwright/.auth/' >> .gitignore
Playwright’s authentication guide warns that saved state can contain sensitive cookies and headers that could be used to impersonate an account. Keep the file private, limit who and what can read it, and remove or refresh it when it expires or is no longer needed. Playwright authentication documentation
3. Reuse the state, verify the page, and capture it
Load the state into a new isolated context, navigate to the target, and confirm both the destination and a page-specific authenticated signal before taking the screenshot. This prevents a login redirect or an access-denied page from being mistaken for the requested content.
Create capture.mjs:
import { chromium } from 'playwright';
const targetUrl = process.env.TARGET_URL;
if (!targetUrl) throw new Error('Set TARGET_URL.');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
try {
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
// Replace with a reliable signal that identifies the expected signed-in page.
await page.getByRole('heading', { name: /account overview/i }).waitFor({
state: 'visible',
timeout: 30_000,
});
const current = new URL(page.url());
if (current.origin !== new URL(targetUrl).origin) {
throw new Error(`Unexpected redirect to ${current.origin}`);
}
await page.screenshot({
path: 'page.png',
fullPage: true,
animations: 'disabled',
});
console.log(`Saved authenticated screenshot from ${page.url()}`);
} finally {
await context.close();
await browser.close();
}
Run it with the target URL, for example:
TARGET_URL='https://example.com/account/overview' node capture.mjs
The heading and origin checks are examples, not universal authentication tests. Choose a stable indicator that proves the intended content is present. A page can remain on the same origin while showing an expired-session message, so check the page content too.
What “authenticated cookies” can mean
Many sites authenticate with cookies, but a browser’s signed-in state may depend on more than cookies. Playwright storage state can include cookies, local storage, and, when requested, IndexedDB. Some authentication schemes also use session storage; Playwright documents that ordinary storage-state reuse does not persist session storage. Check the site’s authentication model if a saved state appears to load but the session is still anonymous. Authentication state details
Browser contexts are independent sessions. A non-persistent context is isolated from other contexts and does not write its browsing data to disk as a persistent browser profile would. The explicit storageState file is a separate artifact that your code chooses to save and reuse. BrowserContext documentation
Screenshot options: scope, quality, and stability
| Need | Option or approach | Trade-off |
|---|---|---|
| What is visible now | Default screenshot | Captures the current viewport only. |
| All scrollable page content | fullPage: true |
Can create a very tall, memory-heavy image; lazy content may need to be scrolled into view first. |
| A known region | clip: { x, y, width, height } |
Coordinates define a rectangle; use a locator screenshot when the desired region is an element. |
| A single element | await page.locator('.report').screenshot({ path: 'report.png' }) |
Wait for the element to be visible and ensure it is not covered or clipped. |
| Higher pixel density | deviceScaleFactor in browser.newContext(), or screenshot scale where supported |
More pixels increase file size and processing needs. CSS scale uses CSS-pixel dimensions; device scale uses device-pixel ratio. |
| Consistent output | animations: 'disabled' |
Changes animation behavior during capture; use only when a stable frame matters more than capturing a live animation. |
| Hide visible sensitive content | mask: [page.locator('.private-value')] |
Masks affect the image only. They do not protect browser state, page data, logs, or other artifacts. |
| Choose file type | Set path to a supported extension such as .png or .jpeg |
PNG is lossless; JPEG is typically smaller for photographic content. Check Playwright’s API for current supported formats and options. |
Screenshot API reference: Playwright Page screenshot options.
For a full-page capture with lazy-loaded content, scroll through the page before capturing so content that loads on visibility has a chance to appear:
await page.evaluate(async () => {
await new Promise((resolve) => {
let previousHeight = 0;
const step = Math.max(400, window.innerHeight);
const tick = () => {
window.scrollBy(0, step);
const height = document.documentElement.scrollHeight;
if (window.scrollY + window.innerHeight >= height && height === previousHeight) {
resolve();
return;
}
previousHeight = height;
setTimeout(tick, 150);
};
tick();
});
});
await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({ path: 'page.png', fullPage: true });
For pages that continuously append content, set an application-appropriate maximum scroll duration or stopping condition; an unbounded scroll loop can run indefinitely.
Let an AI agent choose the page, not the credentials
A practical agent workflow separates the decision about what to capture from the secret used to authenticate:
- Give the agent a narrow task, such as capturing a specific report page in an account it is authorized to use.
- Load the state file from a protected runtime location. Do not include its contents in a prompt, source file, trace shared with others, or agent output.
- Have the agent navigate to the requested URL and inspect the resulting URL and page-specific signal.
- Capture only after those checks pass. If the agent cannot establish that it reached the intended signed-in view, fail the task instead of saving a misleading screenshot.
- Store and share the image according to the sensitivity of the page. The screenshot can contain private information even when the browser state is kept separate.
Authentication state is scoped to the site and authentication model that produced it. It may expire, be revoked, or be invalidated by security changes. Do not assume one state file works for every account, domain, or browser task.
cURL, Python, and Node.js alternatives
Playwright is the direct choice when the workflow needs a browser session, normal interactive sign-in, page verification, and rendered-page controls. Plain HTTP clients can save an image only when they can reach the rendered content and have the right session cookies; they do not execute a full browser workflow by themselves.
For a site whose session is cookie-based and whose HTML can be rendered by an external screenshot service, you can pass the relevant cookie through a service that supports custom cookies. Do not paste a live session cookie into a shell history or commit it to code. The following ScreenshotNeo examples capture a public URL; its documented API options are at ScreenshotNeo documentation.
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()
open("shot.webp", "wb").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()))
);
For authenticated content, use a workflow and service configuration that support the site’s authentication requirements. ScreenshotNeo supports custom cookies and Authorization headers, but only use credentials for accounts and pages you are authorized to access. Its request parameters include names used by other screenshot APIs to make switching easier. See the API documentation for the current request options.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Make one request to capture a page:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Cookie banners are accepted like a visitor and removed before the shot, along with known newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, 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. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and start with 1,000 screenshots a month at no charge and no card.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The target shows a login page | State was not saved after login, has expired, or does not cover the site’s authentication mechanism. | Repeat the normal sign-in flow, wait for a signed-in signal, save fresh state, and confirm whether the site relies on session storage or another mechanism. |
| Login locator times out | The site’s labels or button names differ from the example, or the login form has not loaded. | Inspect the page and use locators matching its actual accessible labels. Wait for the form before filling it. |
| Success check times out | The example heading or navigation signal is not present, or login did not complete. | Choose a real, stable page-specific signal. Check the current URL and visible error messages before saving state. |
| Screenshot is blank or incomplete | Capture began before content rendered, content loads after scrolling, or a redirect/error page was captured. | Wait for the expected page element, scroll to trigger lazy content, and check the final URL and page identity before capture. |
| State file is missing | The output directory does not exist or the capture runs from a different working directory. | Create the directory before saving and use a path resolved from the script directory for automation that changes working directories. |
| Screenshot is unexpectedly huge | fullPage captured a long document or device-pixel scaling increased image dimensions. |
Use viewport or element capture, set an appropriate scale, or capture a defined clip. |
| Page content changes between captures | Animations, dynamic data, or asynchronous loading are still active. | Wait for a stable page signal; disable animations if that matches the desired output, and avoid capturing during updates. |
| Masked data is still exposed elsewhere | A screenshot mask hides pixels but does not sanitize the browser state or page artifacts. | Protect the state file and other outputs separately; mask only addresses visible pixels in the screenshot. |
Performance, reliability, and cost
Browser startup, authentication redirects, network latency, and page rendering all contribute to capture time. Reusing a saved state avoids repeating interactive login, but it does not remove the time required to load the target page. Use explicit timeouts and wait for the specific content needed rather than waiting for every network connection to become idle; some pages keep analytics or streaming requests open.
Full-page screenshots use more memory and produce larger files as page height and device scale increase. Capture only the viewport, a locator, or a clip when that is enough. For recurring jobs, refresh state through the normal sign-in process when it expires and treat failed authentication as a task failure rather than retrying indefinitely.
Playwright is open-source browser automation software; this workflow’s direct costs depend on where the browser runs and the compute and storage it uses. For ScreenshotNeo, only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Check the response’s X-Page-Verdict and X-Billed headers. Plans are Free: 1,000 shots/month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan.
FAQ
Can I reuse one authenticated state file for multiple captures?
Yes, if the same account, site, and authentication mechanism accept that state. Protect the file like a credential and expect it to expire or be revoked.
Does a screenshot prove that a page was authenticated?
No. A screenshot records pixels. Check the destination and a reliable signed-in page signal before capture, and make those checks part of the task’s success criteria.
Can the AI agent see the saved cookies?
It does not need to. The browser automation can load protected state at runtime while the agent receives only the task and the page-level result needed to decide whether capture succeeded.
Does a mask remove private data from the saved browser state?
No. A mask obscures matching content in the screenshot image only. Protect or delete state files and other artifacts separately.


