Chrome Headless Screenshot with Cookies and Session Storage: How to Set It Up
Set cookies and sessionStorage before a page’s scripts run, wait for the right UI state, then capture a reliable Chrome Headless screenshot.
Direct answer: Use Playwright with headless Chromium. Add cookies to a browser context with context.addCookies(), register an origin-guarded context.addInitScript() to seed sessionStorage, and only then navigate. Wait for a page-specific ready signal before calling page.screenshot().
Cookies and session storage are separate pieces of browser state. Playwright’s storage state workflow handles cookies and other documented persisted state, while session storage needs its own save-and-restore step. The order matters: the initialization script must be installed before the page’s application scripts read session storage. See the [Playwright authentication guide](https://playwright.dev/docs/auth) and [BrowserContext API](https://playwright.dev/docs/api/class-browsercontext).
1. Install Playwright and Chromium
This example uses JavaScript modules and Node.js. Create a project and install Playwright plus its Chromium browser:
npm init -y
npm install playwright
npx playwright install chromium
Save the script below as screenshot.mjs. Set the session cookie and JSON session-storage values in the environment, then run node screenshot.mjs. The example targets example.com; replace the domain, URL, cookie attributes, values, and readiness condition to match your site.
2. Add cookies and session storage before navigation
import { chromium } from 'playwright';
const targetUrl = 'https://example.com/dashboard';
const hostname = new URL(targetUrl).hostname;
const sessionCookie = process.env.SESSION_COOKIE;
if (!sessionCookie) throw new Error('Set SESSION_COOKIE');
const sessionStorageValues = JSON.parse(
process.env.SESSION_STORAGE_JSON ?? '{}'
);
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
await context.addCookies([
{
name: 'session-id',
value: sessionCookie,
domain: hostname,
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Lax',
},
]);
// Runs after document creation and before page scripts. The hostname guard
// keeps these values from being written to unrelated origins.
await context.addInitScript(({ expectedHostname, values }) => {
if (window.location.hostname === expectedHostname) {
for (const [key, value] of Object.entries(values)) {
window.sessionStorage.setItem(key, String(value));
}
}
}, { expectedHostname: hostname, values: sessionStorageValues });
const page = await context.newPage();
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
// Prefer a real signal for the state you want to capture.
await page.locator('[data-page-ready="true"]').waitFor({ state: 'visible' });
await page.screenshot({ path: 'screenshot.png', fullPage: true });
await context.close();
} finally {
await browser.close();
}
Example invocation:
SESSION_COOKIE='replace-with-a-valid-session' \
SESSION_STORAGE_JSON='{"active-workspace":"demo","view":"overview"}' \
node screenshot.mjs
The readiness selector is intentionally site-specific. Replace it with an element that appears only when the authenticated view is ready. If the app has no such marker, wait for a known heading, URL change, or other observable condition rather than relying on an arbitrary delay.
3. Cookie and storage details that affect authentication
Cookie domain, path, and security attributes
Playwright requires cookie domain and path when adding a cookie in this form. Match the site’s actual cookie scope and attributes. A cookie scoped to example.com is not automatically a cookie for a different host; a leading dot can make a domain cookie apply to subdomains. Set secure: true for HTTPS sessions and use the appropriate sameSite setting. The session cookie’s name and value must come from a valid session for that site.
Cookie flags are not interchangeable. httpOnly controls whether page JavaScript can read a cookie; it does not prevent the browser from sending it. If a site expects a host-only cookie, use the corresponding host-only configuration supported by the Playwright version in use instead of broadening its scope.
Session storage is origin-scoped
sessionStorage belongs to an origin (scheme, host, and port) and a browsing context. Seed it on the exact origin where the app reads it. The hostname guard above protects against writing to a different hostname; if the same hostname serves different applications on different schemes or ports, guard the full origin too:
const expectedOrigin = 'https://example.com';
await context.addInitScript(({ origin, values }) => {
if (window.location.origin === origin) {
for (const [key, value] of Object.entries(values)) {
window.sessionStorage.setItem(key, String(value));
}
}
}, { origin: expectedOrigin, values: sessionStorageValues });
Values in web storage are strings. Serialize objects before passing them, and parse them in the application if that is what its storage format expects. Do not assume an authentication token stored in session storage can replace a server session cookie; many apps require both, or use a different auth flow entirely.
Storage state for repeatable sessions
For repeatable runs, Playwright can save context state with storageState() and load it into a later context. Its documented state covers cookies, local storage, IndexedDB, and virtual WebAuthn credentials; handle session storage separately with an initialization script. Treat saved state as a credential: it can contain cookies or headers that grant account access. Keep it out of public repositories and use a limited test account where practical.
4. Wait for the visual state, then capture
Navigation completion is not necessarily application readiness. A client-rendered page may still be fetching data, hydrating components, or showing a loading state after domcontentloaded. Choose a wait condition tied to the screenshot’s intended content.
- Known element: wait for the authenticated page heading or a stable element.
- Application marker: expose a test-only ready marker after rendering finishes and wait for it.
- URL state: wait for the expected route if authentication redirects before reaching the target page.
- Short delay: use only when the app has a known animation or delayed render with no better signal.
Then choose the output options: fullPage: true captures the full document; omit it for the current viewport. path writes a file, and screenshot options also support returning image bytes for downstream processing. Set the browser context viewport and device scale factor to control the layout and pixel density.
5. Puppeteer and Chrome Headless
Chrome Headless is Chrome running without its normal visible browser window. Puppeteer provides page.screenshot(), and browser contexts isolate sessions so their cookies and local storage are not shared. The reliable sequence remains: establish the intended context state, install origin-scoped session storage before app code depends on it, navigate, wait for the target UI, then capture. See [Chrome Headless mode](https://developer.chrome.com/docs/chromium/headless) and the [Puppeteer Page API](https://pptr.dev/api/puppeteer.page.screenshot).
The concrete session-storage seeding recipe here uses Playwright’s documented initialization-script approach. Puppeteer APIs and setup details vary by version, so consult the documentation for the Puppeteer version you use rather than assuming an API name or lifecycle behavior.
6. cURL, Python, and Node.js with ScreenshotNeo
If you already have a page that is publicly reachable and does not need your private browser session, a screenshot API can avoid launching and maintaining a browser for each capture. ScreenshotNeo is a website screenshot API and MCP server from [Yorker Media](https://screenshotneo.com). Its request accepts a URL and returns an image or PDF. It does not import your local browser cookies or session storage, so use the DIY browser flow above when the page requires your private authenticated state.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.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://example.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())));
See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for request parameters and response details. ScreenshotNeo can remove cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step 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. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| You are redirected to login | The cookie is expired, scoped to another host/path, or not the session the app expects. | Use a fresh session cookie from the correct environment. Match its domain, path, secure, and SameSite settings; check whether the app also requires a CSRF cookie or another credential. |
| The app ignores session storage | The script was registered after navigation, the key/value format is wrong, or the app is on another origin. | Install addInitScript before creating/navigating the page. Check the exact origin and key names, and store values in the format the app expects. |
| Storage appears on the wrong pages | The initialization script lacks a sufficiently narrow origin check. | Guard on location.origin or hostname as appropriate. Keep the script scoped to the target site. |
| Screenshot shows a spinner or skeleton | Navigation finished before the application finished rendering. | Wait for a specific ready element or application signal before capture. Avoid treating network-idle as proof that the desired UI is ready. |
| Cookie is rejected or not sent | Cookie attributes conflict with the target URL, such as a secure cookie on HTTP or a mismatched domain. | Use HTTPS for secure cookies and configure the cookie scope to match the request host. Verify the target URL and cookie attributes. |
| Unexpected data from a previous run | State was reused in a persistent profile or a context was not isolated. | Create a fresh browser context per isolated capture, or deliberately load a known storage state. Close contexts after use. |
| Session works in one tab but not another | Session storage is tied to a browsing context as well as an origin. | Seed it in the context before creating the page or use an initialization script that runs for each new page/frame as needed. |
8. Performance, reliability, and cost
Launching a browser has a fixed setup cost; reusing a browser process while creating a fresh context per job can reduce repeated launch overhead while keeping session data isolated. Close pages, contexts, and the browser cleanly. Full-page screenshots of long pages and high device scale factors use more memory and produce larger files than viewport captures.
For reliable captures, use explicit timeouts, a site-specific readiness condition, and bounded retries for transient navigation failures. Do not blindly retry authentication failures, because repeating an invalid session will not fix it. Protect cookies and storage-state files as secrets, and avoid logging their values. A self-managed browser has infrastructure and maintenance costs; an API shifts browser operation to the provider and may charge by successful capture according to its plan. ScreenshotNeo’s stated plans range from 1,000 free monthly shots to paid tiers starting at $5 for 3,000; its billing headers identify whether a response was billed.
FAQ
Does Playwright storageState include sessionStorage?
Session storage is handled separately in Playwright’s documented authentication workflow. Save and restore the values with an initialization script for the target origin.
Can I use this for an authenticated page?
Yes, if you can supply valid credentials in the form the application expects. This example seeds a cookie and session storage; some sites require additional state or a fresh login flow.
Will a screenshot API use my browser’s login session?
No. The ScreenshotNeo URL request does not transfer the cookies or session storage from your local browser. Use the local Playwright setup for private authenticated pages.
Should I use a persistent profile?
Use one when retaining a browser session between runs is necessary and you can protect the profile. For isolated captures, a fresh context with explicitly supplied state is easier to reason about.


