The renderer starts every capture as a new visitor with no cookies. To capture a page that needs a login, you send the credential with the request. You will pick the right method, send it, and make sure that a wrong credential gives an error instead of a picture of the login form.
Pick the method that matches the site
| The page is protected by | Send | Typical sites |
|---|---|---|
| A login form and a session cookie | network.cookies |
SaaS dashboards, admin panels |
| HTTP basic auth (a browser password box) | network.auth, type: "basic" |
Staging and preview sites |
| A bearer token | network.auth, type: "bearer" |
Internal report pages |
| A custom header | network.headers |
Tenant IDs, preview tokens |
These options work with POST only. Never put credentials in a URL: URLs end up in logs.
Without a credential, the site sends the renderer to its login page. That is what you get as an image:
Get a session and send the cookie
Create a user just for screenshots, with read-only access. Then, in your capture job, sign in over HTTP, read the session cookie, and pass it to the API:
// capture-dashboard.mjs: Node 20+, no dependencies import { writeFile } from 'node:fs/promises'; const APP = 'https://app.example.com'; // 1. Sign in like a browser, but keep the redirect so you can read Set-Cookie. const login = await fetch(`${APP}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ email: process.env.SCREENSHOT_USER, password: process.env.SCREENSHOT_PASSWORD }), redirect: 'manual', }); const setCookie = login.headers.getSetCookie().find((cookie) => cookie.startsWith('sid=')); if (!setCookie) throw new Error(`login failed with HTTP ${login.status}`); const sid = setCookie.split(';')[0].slice('sid='.length); // 2. Capture with the cookie. const response = await fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: `${APP}/dashboard`, network: { cookies: [{ name: 'sid', value: sid, domain: 'app.example.com', path: '/', secure: true, http_only: true }], }, wait: { for_selector: '.account-menu', timeout: 20 }, cache: { ttl: 3600 }, }), }); if (!response.ok) throw new Error(`capture failed: ${response.status} ${await response.text()}`); await writeFile('dashboard.png', Buffer.from(await response.arrayBuffer()));
The image at the top of this guide shows that the cookie arrives: httpbin.org/cookies lists every cookie it receives.
Most cookie problems come from three fields:
domainmust match the page's host, or a parent domain the app uses (example.comforapp.example.com).secure: trueis needed when the app marks its cookieSecure, which HTTPS apps usually do.- Some apps need two cookies (a session and a CSRF cookie). Copy every cookie the browser sends, from your browser's developer tools.
Use basic auth or a token
For a staging site with a browser password box:
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" -H "Content-Type: application/json" \ -d '{ "url": "https://staging.example.com/", "network": { "auth": { "type": "basic", "username": "staging", "password": "'"$STAGING_PASSWORD"'" } } }' --output staging.png
For a page that takes a bearer token, and a custom header for the tenant:
{ "url": "https://app.example.com/report", "network": { "auth": { "type": "bearer", "token": "<token>" }, "headers": { "X-Tenant": "acme" } } }
The API sends these headers only to the page's own origin. Scripts and images from other hosts do not get them.
Make the login page fail
When a session expires, the app shows its login page, and you get a sharp image of a login form. Prevent that: wait for an element that only signed-in users see, such as the account menu.
{ "url": "https://app.example.com/dashboard", "network": { "cookies": [{ "name": "sid", "value": "<session>", "domain": "app.example.com" }] }, "wait": { "for_selector": ".account-menu", "timeout": 20 } }
If the login page loads instead, the wait times out and the request fails with timeout. Failed requests cost no credits. When you see that error, sign in again and retry once.
Keep the screenshots private
Screenshots of private pages need extra care:
- The image has a public CDN link (
X-Cache-URL) until the cache entry expires. Set"cache": { "ttl": 3600 }, the minimum, and purge the entry when you have the file: send theX-Cache-Keyheader value toPOST /v1/cache/purge(cache invalidation). - Logs hide credentials. Cookie values and
network.authvalues show as[FILTERED]in your request logs. Customnetwork.headersare stored as sent, so put secrets innetwork.authornetwork.cookies. - Short-lived sessions. Sign in once per job and sign out after.
- Server only. These requests carry your API key and the app credential. Never send them from a browser.
Next steps
- Fix blank or incomplete screenshots for dashboards that load data late.
- Save screenshots to your bucket with private access.
- Prepare for production.