# 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.

```bash
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.png
```

You 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](#6-your-secrets-go-to-that-site-only).

## 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`](https://curlshot.com/docs/options.md#cookies) |
| A token in a header | A secret sent in an HTTP header, a labelled line that travels with each request. | [`headers`](https://curlshot.com/docs/options.md#headers) |
| HTTP authentication | The browser shows a plain grey box that asks for a name and password. | [`authorization`](https://curlshot.com/docs/options.md#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.

1. Press `F12`, or right-click the page and choose **Inspect**.
2. Open the **Application** tab in Chrome or Edge. In Firefox and Safari it is called **Storage**.
3. In the left column, open **Cookies** and click the address of the site.
4. Look for the cookie that holds the session. The name often contains `session`, `sid` or `auth`.
5. 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`.

```bash
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.png
```

The 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:

```bash
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.png
```

The 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:

```bash
# 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.png
```

The result is `staging.png`: the page behind the password box.

For a bearer token, send `"authorization": "Bearer YOUR_SITE_TOKEN"`.

> **Two different Authorization values**
>
> The `authorization` option goes to the site you capture. It is not your access key. Your access key goes to us, in the `X-Access-Key` header. Keep the two apart, and never put your access key in the `authorization` option.

An address with a name and password inside it, such as `https://user:pass@your-app.example`, is refused with [`invalid_options`](https://curlshot.com/docs/errors.md#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.

> **One consequence**
>
> If your page loads data from a second host of yours, such as `api.your-app.example`, the `authorization` value and credential headers are not sent there. Use a cookie with `"domain": ".your-app.example"`, which covers the subdomains, the way it would in a browser.

## 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](https://curlshot.com/docs/signed-links.md). 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`](https://curlshot.com/docs/options.md#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`](https://curlshot.com/docs/options.md#timeout), which is 30 seconds by default, you get a [`selector_not_found`](https://curlshot.com/docs/errors.md#selector_not_found) error and no screenshot is counted. Then sign in again and copy the new cookie value.

```bash
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"
  }'
```

```json
{
  "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`](https://curlshot.com/docs/options.md#click) option clicks one element, and [`scripts`](https://curlshot.com/docs/options.md#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`](https://curlshot.com/docs/errors.md#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.

> **Common mistakes**
>
> - **The screenshot shows the login page.** The cookie is wrong, expired, or set for another domain. Check the `domain` field against the **Domain** column in your browser.
> - **The cookie is tied to your IP address or browser.** Some sites reject a session that suddenly comes from another place. Create the session from a setup the site accepts, or use a token made for automation.
> - **You put the cookie in a `GET` query string.** It works, but the secret is now in logs. Use `POST`.
> - **A token header does not reach your second host.** Credentials go only to the origin of `url`. See [step 6](#6-your-secrets-go-to-that-site-only).

## Where to go next

- [Waiting and timing](https://curlshot.com/docs/waiting.md): make sure the signed-in page is fully loaded before the capture.
- [The screenshot URL](https://curlshot.com/docs/screenshot-url.md): more on `POST` requests and JSON bodies.
- [Authentication and API keys](https://curlshot.com/docs/authentication.md): how your own access key is sent.
- [Options reference](https://curlshot.com/docs/options.md#cookies): every request option in one place.
