> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Log in once and reuse the session

> Log in to a site once in Zenrows Browser, save the session, and start every later session already signed in.

Log in to a site once in Zenrows Browser, the browser for agents, save the session state to a file, and load it into every new browser session. Your scripts and agents skip the login form on later runs, which makes them faster and keeps the number of logins low.

**Uses:** [Browser](/browser-sessions/introduction) with Playwright's `storage_state`, and the MCP tools `browser_get_cookies` / `browser_set_cookies`

## Ask your AI agent

With the [Zenrows MCP server](/mcp/overview) connected, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.

```text theme={"dark"}
Use the Zenrows browser to log in at https://www.scrapingcourse.com/login with
email admin@example.com and password password. Confirm the dashboard shows the
product grid, list the first product name, and show me the session cookie names.
Then close the browser session.
```

To reuse the login later, ask the agent to "open a new Zenrows browser session with the same cookies" before it visits the protected page.

## Log in and save the session state

<Warning>
  Automate logins only for accounts you own or are authorized to use, and follow the site's terms.
</Warning>

The demo login at `scrapingcourse.com/login` accepts `admin@example.com` / `password`. After you submit the form, wait for an element that appears after the login (here, the "Welcome" banner), then save the state with `storage_state`. The file holds cookies and local storage.

<CodeGroup>
  ```python Python (Playwright) theme={"dark"}
  import asyncio
  from playwright.async_api import async_playwright

  CONNECTION_URL = "wss://browser.zenrows.com?apikey=YOUR_ZENROWS_API_KEY"

  async def main():
      async with async_playwright() as p:
          browser = await p.chromium.connect_over_cdp(CONNECTION_URL)
          try:
              context = browser.contexts[0]
              page = await context.new_page()
              await page.goto("https://www.scrapingcourse.com/login")
              await page.fill("#email", "admin@example.com")
              await page.fill("#password", "password")
              await page.click("#submit-button", no_wait_after=True)
              # Wait for proof of login rather than for the navigation
              await page.get_by_text("Welcome, Scraper!").wait_for()
              print("Logged in:", await page.get_by_text("Welcome, Scraper!").inner_text())
              await context.storage_state(path="auth.json")
          finally:
              await browser.close()

  asyncio.run(main())
  ```

  ```javascript Node.js (Playwright) theme={"dark"}
  import { chromium } from "playwright-core";

  const connectionURL = "wss://browser.zenrows.com?apikey=YOUR_ZENROWS_API_KEY";

  const browser = await chromium.connectOverCDP(connectionURL);
  try {
    const context = browser.contexts()[0];
    const page = await context.newPage();
    await page.goto("https://www.scrapingcourse.com/login");
    await page.fill("#email", "admin@example.com");
    await page.fill("#password", "password");
    await page.click("#submit-button", { noWaitAfter: true });
    // Wait for proof of login rather than for the navigation
    await page.getByText("Welcome, Scraper!").waitFor();
    console.log("Logged in:", await page.getByText("Welcome, Scraper!").innerText());
    await context.storageState({ path: "auth.json" });
  } finally {
    await browser.close();
  }
  ```
</CodeGroup>

Waiting for a post-login element is the reliable signal for any login form: it confirms the credentials worked, whatever redirects the site runs in between.

## Reuse the session in a new browser

Open a new browser session, create a context from `auth.json`, and go straight to the protected page.

<CodeGroup>
  ```python Python (Playwright) theme={"dark"}
  import asyncio
  from playwright.async_api import async_playwright

  CONNECTION_URL = "wss://browser.zenrows.com?apikey=YOUR_ZENROWS_API_KEY"

  async def main():
      async with async_playwright() as p:
          browser = await p.chromium.connect_over_cdp(CONNECTION_URL)
          try:
              context = await browser.new_context(storage_state="auth.json")
              page = await context.new_page()
              await page.goto("https://www.scrapingcourse.com/dashboard")
              await page.get_by_text("Welcome, Scraper!").wait_for()
              names = await page.locator(".product-name").all_inner_texts()
              print("Second session, still logged in. Products:", names[:3])
          finally:
              await browser.close()

  asyncio.run(main())
  ```

  ```javascript Node.js (Playwright) theme={"dark"}
  import { chromium } from "playwright-core";

  const connectionURL = "wss://browser.zenrows.com?apikey=YOUR_ZENROWS_API_KEY";

  const browser = await chromium.connectOverCDP(connectionURL);
  try {
    const context = await browser.newContext({ storageState: "auth.json" });
    const page = await context.newPage();
    await page.goto("https://www.scrapingcourse.com/dashboard");
    await page.getByText("Welcome, Scraper!").waitFor();
    const names = await page.locator(".product-name").allInnerTexts();
    console.log("Second session, still logged in. Products:", names.slice(0, 3));
  } finally {
    await browser.close();
  }
  ```
</CodeGroup>

## Example output

```text theme={"dark"}
Logged in: Welcome, Scraper!
Second session, still logged in. Products: ['Chaz Kangeroo Hoodie', 'Teton Pullover Hoodie', 'Bruno Compete Hoodie']
```

The saved `auth.json` holds the site's cookies, including the login session:

```text theme={"dark"}
_ga, AWSALB, AWSALBCORS, XSRF-TOKEN, scrapingcoursecom_session, _ga_NZGD14H87G
```

## Customize it

| Change | How |
| - | - |
| Keep the same IP country across runs | Add `&proxy_country=us` to the URL of both sessions, see [Country targeting](/browser-sessions/features/country) |
| Stay logged in longer in one run | Add `&session_ttl=10m`, see [Session TTL](/browser-sessions/features/session-ttl) |
| Cookies without local storage | `await context.cookies()` to save, `await context.add_cookies(cookies)` to restore |
| Log in from an AI assistant | `browser_get_cookies` and `browser_set_cookies`, see the [MCP tool reference](/mcp/tool-reference) |
| Log in from the terminal | `zenrows browser cookies --session <id>` and `zenrows browser set-cookies --session <id> --cookies '[...]'`, see [Let your AI assistant browse](/academy/browser/ai-assistant-browsing) |

## Troubleshooting

* **`page.click` waits until it times out after submitting the form**: Playwright is waiting for the navigation to settle. Click with `no_wait_after=True` (`noWaitAfter: true` in Node.js) and wait for an element of the logged-in page instead.
* **The second session lands on the login page**: the saved session expired or the site ties it to an IP. Log in again to refresh `auth.json`, and use the same `proxy_country` on both sessions.
* **`auth.json` has no session cookie**: the state was saved before the login finished. Save it after the post-login element appears.

<Warning>
  `auth.json` grants access to the account. Store it like a password: keep it out of version control and share it with the processes that need it.
</Warning>

## Related recipes

<CardGroup cols={2}>
  <Card title="Fill and submit multi-step forms" icon="forms" href="/academy/browser/fill-forms">
    Type, select and submit, then verify the result on the page.
  </Card>

  <Card title="Connect Playwright or Puppeteer to Zenrows Browser" icon="plug" href="/academy/browser/connect-playwright-and-puppeteer">
    The connection URL and its country, region and lifetime options.
  </Card>

  <Card title="Let your AI assistant browse with Browser tools" icon="message-chatbot" href="/academy/browser/ai-assistant-browsing">
    Log in, click and read pages from Claude, Cursor or the CLI.
  </Card>

  <Card title="Keep the same IP and submit forms" icon="cookie" href="/academy/fetch/sessions-and-forms">
    Reuse an IP and submit forms with Fetch, no browser needed.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.