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

# Sessions

> Get a browser that is already logged into an account — as a CDP URL you drive with your own Playwright or Puppeteer, or as a portable storageState you run on your own infrastructure.

An account is only useful if your code can act as it. Sente hands the login back in three shapes:

| Shape                       | For                                                                                                         | Sente in the hot path?                   |
| --------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| **CDP URL**                 | Attaching your existing Playwright/Puppeteer/browser-use code to a remote browser that is already signed in | Yes — the browser runs on our side       |
| **Exported `storageState`** | Loading cookies and localStorage into your own browser, on your own infrastructure                          | No — after the export you're on your own |
| **Live view URL**           | A human to watch or take over                                                                               | Yes                                      |

You don't change your automation stack for any of them. The point is that your agent gets a
signed-in browser and never holds the password.

## Open a live session (CDP)

`POST /v1/registrations/:id/session` opens a remote browser on the account's logged-in profile and
returns a CDP URL. Connected accounts use the same call with the connection id.

<CodeGroup>
  ```bash CLI theme={null}
  CDP=$(sente session open <registrationId>)
  # stdout is JUST the CDP url, so it pipes cleanly; live view + expiry go to stderr.
  # Add --no-verify to skip the freshness check and take the session as-is.

  sente session close <registrationId>   # idempotent
  ```

  ```ts TypeScript theme={null}
  import { chromium } from "playwright";

  const session = await sente.registrations.getSession(registration.id);
  // { sessionId, cdpUrl, liveViewUrl, expiresAt, verified, healed }

  const browser = await chromium.connectOverCDP(session.cdpUrl);
  try {
    const context = browser.contexts()[0] ?? (await browser.newContext());
    const page = context.pages()[0] ?? (await context.newPage());
    await page.goto("https://app.example.com/settings");
    // …drive the app as the account…
  } finally {
    await browser.close();
    await sente.registrations.closeSession(registration.id);
  }
  ```

  ```python Python theme={null}
  from playwright.sync_api import sync_playwright

  session = sente.registrations.get_session(registration_id)
  # { "sessionId", "cdpUrl", "liveViewUrl", "expiresAt", "verified", "healed" }

  with sync_playwright() as p:
      browser = p.chromium.connect_over_cdp(session["cdpUrl"])
      try:
          context = browser.contexts[0] if browser.contexts else browser.new_context()
          page = context.pages[0] if context.pages else context.new_page()
          page.goto("https://app.example.com/settings")
          # …drive the app as the account…
      finally:
          browser.close()
          sente.registrations.close_session(registration_id)
  ```
</CodeGroup>

Puppeteer works the same way: `puppeteer.connect({ browserWSEndpoint: cdpUrl })`.

<Note>
  **One live session per identity**, and it is mutually exclusive with runs — both drive the same
  browser profile. Always close the session when you're done (`DELETE
    /v1/registrations/:id/session`, idempotent): it holds a real remote browser and consumes browser
  minutes until you close it or its own `expiresAt` passes.
</Note>

## Freshness and auto re-login

Cookies expire and apps log you out, so `getSession` will not hand you a browser it can't vouch for.
Freshness is measured by **recency of the last confirmed login** — the moment a register, login, or
connect run completed for that account:

* Within the freshness window (about 10 minutes by default), the session is opened straight away and
  you get it in seconds.
* Older, and Sente queues a re-login first: the vaulted credentials, plus a code from the account's
  own inbox (created accounts) or a server-computed TOTP code (connected accounts with a seed). The
  browser you get back was just confirmed logged in. Expect this to take a minute.

The SDKs and the CLI run that loop for you; the SDKs return `healed: true` when a re-login happened.
Pass `maxStaleSec` to widen or tighten the window for one call, or `verify: false`
(CLI `--no-verify`) to skip the check entirely and take whatever the profile holds.

<Note>
  This is recency, not inspection: Sente does not probe the page to test whether you're still signed
  in. A cold call can therefore re-login on cookies that were in fact still valid. It costs a minute;
  it never returns a session it hasn't just confirmed.
</Note>

At the raw API level the loop is visible as `409`s carrying the id you need to poll:

| `409` code             | Means                                                                 | Do                                                                    |
| ---------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `SESSION_STALE`        | The login was stale; a re-login run has been queued.                  | Poll `runId` to completion, then retry the open.                      |
| `SESSION_PENDING`      | A run is already in flight for this identity.                         | Poll `runId`, then retry.                                             |
| `IDENTITY_BUSY`        | Raw open (`verify: false`) while a run is live.                       | Wait for the run, then retry.                                         |
| `SESSION_ALREADY_OPEN` | This identity already has a live session (`sessionId` is returned).   | Reuse it, or close it first.                                          |
| `NOT_ACTIVE`           | The account isn't `active` — still pending, failed, or revoked.       | Finish or retry the register/connect; re-enable a revoked connection. |
| `NO_PROFILE`           | No browser profile yet — no run has ever succeeded for this identity. | Run a register/connect/login first.                                   |

If the healing run itself ends `blocked` — email MFA on a connected account, a CAPTCHA, a wall — the
SDK call raises with that run attached: open its `liveViewUrl`, clear the step, resume, and retry.
See [Human takeover](/runs/human-takeover).

## Export the session — keep your own stack

`POST /v1/registrations/:id/session/export` returns the logged-in browser state (cookies +
localStorage) as a standard Playwright `storageState`. Load it into your own browser, on your own
infrastructure: no CDP connection to Sente, no changes to your existing automation.

<CodeGroup>
  ```bash CLI theme={null}
  sente session export <registrationId> ./state.json
  # writes the storageState JSON with mode 0600
  # (omit the path and it writes ./<registrationId>-storageState.json)
  ```

  ```ts TypeScript theme={null}
  import fs from "node:fs";
  import { chromium } from "playwright";

  const { storageState } = await sente.registrations.exportSession(registration.id);
  fs.writeFileSync("./state.json", JSON.stringify(storageState), { mode: 0o600 });

  const browser = await chromium.launch();
  const context = await browser.newContext({ storageState: "./state.json" });
  const page = await context.newPage();
  await page.goto("https://app.example.com/settings");   // already signed in
  ```

  ```python Python theme={null}
  from playwright.sync_api import sync_playwright

  storage_state = sente.registrations.export_session(registration_id)   # a dict

  with sync_playwright() as p:
      browser = p.chromium.launch()
      context = browser.new_context(storage_state=storage_state)
      page = context.new_page()
      page.goto("https://app.example.com/settings")      # already signed in
  ```
</CodeGroup>

Export runs the same verify-then-heal logic as `getSession` (same `409 SESSION_STALE` /
`SESSION_PENDING` contract), so the state you get was just confirmed logged in. It works for created
and connected accounts alike. If a session is already open, the export is taken from it; otherwise
one is opened for the export and closed again.

<Warning>
  An exported `storageState` is a **bearer credential** for a logged-in account. Anyone holding the
  file can act as that account until the cookies expire — no password needed, and no second factor.
  Keep it out of version control, restrict its permissions, and delete it when you're done. It does
  not stay fresh: when it expires, export again. The same applies to a CDP URL while its session is
  open.
</Warning>

## The live view

Every session — and every [blocked run](/runs/human-takeover) — carries a `liveViewUrl`: an
interactive browser page a person can open to watch or take over. It is how a human enters an MFA
code, solves a CAPTCHA, or clicks a confirm-before-submit final button. Treat the URL as sensitive:
anyone with it controls that browser while the session is open. A blocked run holds its live view
for about 10 minutes; after that the session is torn down and the URL stops working.

## Which shape to use

| You want                                                        | Use                                              |
| --------------------------------------------------------------- | ------------------------------------------------ |
| To drive the app now with your Playwright/Puppeteer code        | `getSession` → `cdpUrl`                          |
| To run the browser on your own infra, Sente out of the hot path | `exportSession` → `storageState`                 |
| A human to look at or act in the browser                        | `liveViewUrl`                                    |
| To re-authenticate without opening a session                    | `registrations.login(id)` / `sente relogin <id>` |
| To hand back the browser and stop the meter                     | `closeSession(id)` / `sente session close <id>`  |

## Next steps

<CardGroup cols={2}>
  <Card title="Human takeover" href="/runs/human-takeover">
    What to do when a healing re-login blocks on a person.
  </Card>

  <Card title="Audit trail" href="/events/audit-trail">
    Every session open, close, and export is recorded — query it via `GET /v1/audit-events`.
  </Card>

  <Card title="Security model" href="/trust/security">
    Why your agent never receives the password, and what the browser agent does see.
  </Card>

  <Card title="API reference: registrations" href="/api-reference/registrations">
    Exact request bodies, response fields, and status codes for the session endpoints.
  </Card>
</CardGroup>
