How to screenshot a page behind a login
Capture a page that needs a signed-in user by sending a session cookie, a header or HTTP credentials, and keep those secrets safe.
To capture a page that only signed-in users can see, send the session cookie of a signed-in user along with the request.
curl -X POST "https://curlshot.com/api/v1/screenshot" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example/dashboard",
"cookies": [
{ "name": "session", "value": "PASTE_THE_COOKIE_VALUE_HERE", "domain": "your-app.example" }
]
}' \
--output dashboard.pngYou get dashboard.png, showing the dashboard the way that user sees it. Without the cookie, the same request would return a screenshot of the login form.
The cookie goes to your-app.example and to nobody else. The same holds for every secret in this guide. See step 6.
#1. Find out how the site keeps you signed in
A website has to recognise you on every page after you log in. There are three common ways it does that. Each one has a matching option.
| The site uses | What that is | Option to use |
|---|---|---|
| A session cookie | A small named value that the browser stores after login and sends with every request. | cookies |
| A token in a header | A secret sent in an HTTP header, a labelled line that travels with each request. | headers |
| HTTP authentication | The browser shows a plain grey box that asks for a name and password. | authorization |
Most websites with a login form use a session cookie. Start there.
#2. Create a separate account for screenshots
Before you copy any secret, make a new user on the site for this job. Do not use your own account.
Give that user as few rights as possible. If the page you want is a read-only report, a read-only user is enough.
A session cookie is as good as a password while it lasts. If it ever leaks, a low-privilege account limits the damage, and you can delete it without locking yourself out.
#3. Copy the session cookie from your browser
Sign in to the site as the new user. Then open the developer tools of your browser.
- Press
F12, or right-click the page and choose Inspect. - Open the Application tab in Chrome or Edge. In Firefox and Safari it is called Storage.
- In the left column, open Cookies and click the address of the site.
- Look for the cookie that holds the session. The name often contains
session,sidorauth. - Copy what is in the Value column. Note the Domain column too.
If you cannot tell which cookie matters, send all of them.
#4. Send the cookie with the request
Use POST and put the cookie in the JSON body. Each cookie is an object with a name and a value.
curl -X POST "https://curlshot.com/api/v1/screenshot" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example/reports/monthly",
"cookies": [
{ "name": "session", "value": "PASTE_THE_COOKIE_VALUE_HERE", "domain": "your-app.example", "secure": true },
{ "name": "theme", "value": "light" }
],
"full_page": true
}' \
--output report.pngThe result is report.png: the full monthly report, as the signed-in user sees it.
#cookies
Cookies to set before the page loads.
There is no default. Leave it out and no cookies are set.
Add domain to say which site the cookie belongs to. If you leave it out, the host of url is used. The optional fields path, secure, http_only, same_site and expires work as they do in a browser.
A cookie can also be written as one line of text, the way a browser shows it: "session=abc123; Domain=your-app.example; Secure". You can send up to 50 cookies.
#5. Or send a header or HTTP credentials
Some sites and many internal tools do not use cookies. Use one of these options for them.
#headers
Extra HTTP headers that the browser sends to the site.
There is no default. Leave it out and no extra headers are sent.
In a POST body, write them as an object of names and values:
curl -X POST "https://curlshot.com/api/v1/screenshot" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example/preview/42",
"headers": { "X-Preview-Token": "YOUR_PREVIEW_TOKEN" }
}' \
--output preview.pngThe result is preview.png: the page as it looks to a request that carries the token.
#authorization
The value of the Authorization header, sent only to the site in url. Use it for HTTP authentication.
There is no default. Leave it out and the header is not sent.
For a name and password (called "Basic" authentication), join them with a colon and encode the result as base64, a way to write any text with plain letters and digits:
# Prints dXNlcjpwYXNz for the name "user" and the password "pass".
TOKEN=$(printf '%s' 'user:pass' | base64)
curl -X POST "https://curlshot.com/api/v1/screenshot" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d "{
\"url\": \"https://your-app.example/staging\",
\"authorization\": \"Basic $TOKEN\"
}" \
--output staging.pngThe result is staging.png: the page behind the password box.
For a bearer token, send "authorization": "Bearer YOUR_SITE_TOKEN".
An address with a name and password inside it, such as https://user:[email protected], is refused with invalid_options. Use the authorization option.
#6. Your secrets go to that site only
A web page loads files from many other places: fonts, analytics scripts, ad networks, video players. None of them should see the login you handed us.
So we send your credentials to one place: the origin of the url you capture. An origin is the scheme, host and port together, such as https://your-app.example.
| What you send | Where it goes |
|---|---|
authorization | Only to requests for the origin of url. |
headers whose name looks like a credential | Only to requests for the origin of url. |
cookies | Only to the cookie's domain, which is the host of url unless you set another. This is how a browser treats any cookie. |
Other headers, such as Accept-Language | To every request the page makes. |
A header name counts as a credential when it contains auth, cookie, token, secret, key or password, in upper or lower case. Authorization, Cookie, X-Api-Key and X-Preview-Token all qualify.
The rule also holds when the page sends the browser somewhere else. If https://your-app.example/dashboard redirects to another site, that site gets none of these headers.
#7. Keep the secret out of URLs and logs
A session cookie in a query string ends up in places you do not control: browser history, server logs, proxy logs. A POST body does not.
Follow these rules:
- Send cookies, tokens and credentials with
POST, in the JSON body. All samples in this guide do. - Never put them in a link that a browser loads, such as an
<img>tag. That includes signed links. Signing stops changes to a link. It does not hide what is in it. - Call the API from your server. Store the cookie value where you store other secrets, not in your code.
On our side, cookie values, the authorization value and headers with names that look like secrets are masked before a request is written to our logs.
#8. Plan for the session to expire
A session does not last forever. Sites end it after a set time, or when the user logs out.
When that happens, the request still succeeds. You get a sharp screenshot of the login page, and it counts as a normal screenshot.
To catch this, add wait_for_selector with an element that only exists when you are signed in, such as #account-menu. A selector is a short pattern that names an element on the page. Here it means "the element with the id account-menu".
If the session is gone, the element never shows up. After the timeout, which is 30 seconds by default, you get a selector_not_found error and no screenshot is counted. Then sign in again and copy the new cookie value.
curl -X POST "https://curlshot.com/api/v1/screenshot" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example/dashboard",
"cookies": [{ "name": "session", "value": "AN_EXPIRED_VALUE", "domain": "your-app.example" }],
"wait_for_selector": "#account-menu"
}'{
"error_code": "selector_not_found",
"error_message": "No visible element matched wait_for_selector \"#account-menu\" before the timeout.",
"documentation_url": "https://curlshot.com/docs/errors#selector_not_found"
}#What this cannot do
The API does not fill in a login form for you. There is no option that types a name and a password into fields.
The click option clicks one element, and scripts runs JavaScript on the page. Neither is meant for a login in several steps, where you type, a second page loads, and a code is asked for.
So sign in yourself once, then hand the result of that login to the API as a cookie, a header or HTTP credentials.
The page must also be reachable from the public internet. A site on localhost or inside a private network is refused with host_not_allowed.
Ports matter too. The web ports 80 and 443 work, and so does any port from 1024 up, such as 3000 or 8080. Other ports below 1024, and ports that belong to databases and similar services (for example 3306, 5432, 6379, 9200, 11211 and 27017), are refused with the same error.
#Where to go next
- Waiting and timing: make sure the signed-in page is fully loaded before the capture.
- The screenshot URL: more on
POSTrequests and JSON bodies. - Authentication and API keys: how your own access key is sent.
- Options reference: every request option in one place.