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

# Bring your own browser profile

> Start a browser session from saved cookies and storage.

A **browser profile** captures a browser's saved state (cookies, local storage, and session data) as a reusable, organization-scoped bundle. Upload one once, then set [`browser_profile_id`](/computer-use-agents/browser/configuration) so the agent starts a session already authenticated instead of signing in from scratch every run. Sessions can also [write their final state back](#persisting-state-back-into-the-profile) into the profile, so it stays fresh from run to run instead of aging out.

Don't want to manage profiles at all? Your [default profile](#default-profiles) gives you a browser that remembers you across sessions with zero setup — no upload, no ids to track.

Where a [vault](/computer-use-agents/vaults/overview) injects individual secrets at the moment the agent fills a login form, a profile restores the **entire** logged-in state up front. Reach for a vault when the agent should sign in with credentials; reach for a profile to carry an existing, already-authenticated session forward.

Profiles belong to your organization: any agent in the org can load one, and they persist across sessions until you delete them.

## Creating a profile

A profile is built from a browser's **user-data directory**: the folder Chrome uses to store cookies, local storage, saved logins, and extensions.

<Warning>
  Don't upload your personal Chrome directory (`~/Library/Application Support/Google/Chrome` on macOS, `~/.config/google-chrome` on Linux). It holds every password, session token, and history entry from your day-to-day browsing.
</Warning>

The simplest way to get a clean directory is to launch a **separate Chrome instance** pointed at a throwaway `--user-data-dir`. This runs alongside your normal browser without touching your real profile.

<Steps>
  <Step title="Launch a fresh Chrome instance">
    Start Chrome with a new empty directory. It opens a brand-new profile with no cookies, logins, or extensions.

    <CodeGroup>
      ```bash macOS theme={null}
      "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
        --user-data-dir="$HOME/agent-profile"
      ```

      ```bash Linux theme={null}
      google-chrome --user-data-dir="$HOME/agent-profile"
      ```
    </CodeGroup>
  </Step>

  <Step title="Set up the state the agent needs">
    In that window, do exactly what you want the agent to inherit: log in to the target sites, dismiss cookie banners, install extensions, adjust settings. Everything is written into `~/agent-profile`.
  </Step>

  <Step title="Close Chrome and zip the directory">
    Cleanly quit the browser so it flushes its state to disk, then archive the directory:

    ```bash theme={null}
    cd "$HOME/agent-profile"
    zip -r ../profile.zip .
    ```

    The resulting `profile.zip` is what you upload below.
  </Step>
</Steps>

## Uploading a profile

The archive is a `.zip` of the user-data directory you built above. It can hold cookies and saved logins, so treat it as a secret.

A profile archive can be large, so the bytes are uploaded **directly to object storage** through a presigned URL and never pass through the Agent API. The flow is three steps:

<Steps>
  <Step title="Initiate the upload">
    Call [`POST /initiate-upload`](/computer-use-agents/browser-profiles/initiate-upload). It returns a `profile_id`, a presigned `upload_url`, and the `upload_fields` you must include with the upload.

    <CodeGroup>
      ```bash cURL theme={null}
      INIT=$(curl -s -X POST https://agp.eu.hcompany.ai/api/v2/browser-profiles/initiate-upload \
        -H "Authorization: Bearer $HAI_API_KEY")
      PROFILE_ID=$(echo "$INIT" | jq -r .profile_id)
      UPLOAD_URL=$(echo "$INIT" | jq -r .upload_url)
      ```

      ```python Python theme={null}
      from hai_agents import Client

      client = Client()

      upload = client.browser_profiles.initiate_browser_profile_upload()
      ```

      ```typescript TypeScript theme={null}
      import { HaiAgentsClient } from "hai-agents";

      const client = new HaiAgentsClient();

      const upload = await client.browserProfiles.initiateBrowserProfileUpload();
      ```
    </CodeGroup>
  </Step>

  <Step title="Upload the archive to storage">
    `POST` your profile `.zip` to `upload_url` as `multipart/form-data`, including every entry from `upload_fields`. This request goes to object storage, not to the Agent API. The archive must be `application/zip` and at most **1 GB**; storage rejects anything else.

    <CodeGroup>
      ```bash cURL theme={null}
      args=()
      while IFS= read -r line; do
        args+=(--form-string "$line")
      done <<< "$(echo "$INIT" | jq -r '.upload_fields | to_entries[] | "\(.key)=\(.value)"')"

      curl -sS --fail-with-body -w "upload: HTTP %{http_code}\n" \
        -X POST "$UPLOAD_URL" "${args[@]}" -F "file=@profile.zip"

      # Healthy upload will return `upload: HTTP 204`
      ```

      ```python Python theme={null}
      import requests

      with open("profile.zip", "rb") as f:
          resp = requests.post(
              upload.upload_url,
              data=upload.upload_fields,
              files={"file": ("profile.zip", f, "application/zip")},
          )
          resp.raise_for_status()
      ```

      ```typescript TypeScript theme={null}
      import { readFileSync } from "fs";

      const form = new FormData();
      for (const [key, value] of Object.entries(upload.uploadFields)) {
        form.append(key, value as string);
      }
      form.append("file", new Blob([readFileSync("profile.zip")]), "profile.zip");
      const resp = await fetch(upload.uploadUrl, { method: "POST", body: form });
      if (!resp.ok) throw new Error(`upload failed: ${resp.status}`);
      ```
    </CodeGroup>

    The presigned `upload_url` and `upload_fields` are passed through to object storage exactly as returned, so do not modify them. They expire after `upload_expires_in` seconds, so upload promptly after initiating.
  </Step>

  <Step title="Complete the upload">
    Call [`POST /{profile_id}/complete-upload`](/computer-use-agents/browser-profiles/complete-upload) with the profile metadata (`name`, `browser_name`, `browser_version`) **within 1 day** of uploading. The platform verifies the uploaded archive and creates the profile record. Until you complete it, the upload is held as pending and is deleted automatically after 1 day.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST "https://agp.eu.hcompany.ai/api/v2/browser-profiles/$PROFILE_ID/complete-upload" \
        -H "Authorization: Bearer $HAI_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"name": "acme-prod-login", "browser_name": "chromium", "browser_version": "131"}'
      ```

      ```python Python theme={null}
      profile = client.browser_profiles.complete_browser_profile_upload(
          profile_id=upload.profile_id,
          name="acme-prod-login",
          browser_name="chromium",
          browser_version="131",
      )
      print(profile.id)
      ```

      ```typescript TypeScript theme={null}
      const profile = await client.browserProfiles.completeBrowserProfileUpload({
        profileId: upload.profileId,
        name: "acme-prod-login",
        browserName: "chromium",
        browserVersion: "131",
      });
      console.log(profile.id);
      ```
    </CodeGroup>
  </Step>
</Steps>

Once created, manage your profiles with the [browser profile endpoints](/computer-use-agents/browser-profiles/list): list, retrieve, and delete them.

## Persisting state back into the profile

By default a profile is **read-only**: every session starts from the profile's saved state, and whatever the agent does during the run (new cookies, refreshed tokens, dismissed banners) is discarded when the session ends. Over time the profile's sessions expire and agents are back to login walls.

Set `persist_browser_profile: true` alongside `browser_profile_id` (or `use_default_browser_profile`) on the [Browser environment](/computer-use-agents/browser/configuration) to flip that: when the session ends, the browser's final state is written back into the profile, replacing its previous contents. The next session that loads the profile picks up exactly where the last one left off.

```json Browser that keeps its profile fresh theme={null}
{
  "id": "acme-browser",
  "kind": "web",
  "browser_profile_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "persist_browser_profile": true
}
```

A few rules keep concurrent runs from corrupting each other's state:

* **One writer at a time.** Only one active session may persist a given profile; the session holds an exclusive write lock on the profile until it ends. A second session requesting `persist_browser_profile` on the same profile still starts, but runs **read-only** for that round — check the session's effective persist state if the write-back matters to you. Any number of sessions may load the profile read-only in parallel.
* **Last write wins.** Persisting replaces the profile's entire stored state with the session's final state; it is not merged.
* **Decided at create time.** Persistence can only be requested when the session is created, not when attaching to an existing session.
* **Best-effort on stop.** The write-back happens as the session stops. If it fails, the session still stops cleanly and the profile keeps its previous state.

Sessions that follow your [default profile](#default-profiles) persist automatically — no flag needed; see below.

## Default profiles

Every user has a **default profile** per browser flavor (for example one for `chromium`): a browser that remembers you from session to session, with zero setup. Opt a Browser environment into it with `use_default_browser_profile: true`:

```json Browser that follows the default profile theme={null}
{
  "id": "acme-browser",
  "kind": "web",
  "use_default_browser_profile": true
}
```

* **First use bootstraps it.** If you have no default yet, an empty profile is created and marked as your default automatically when the session starts — no upload, no extra API call. Log in to your sites once (for example through the session's live view) and the state carries forward from then on.
* **Sessions save back automatically.** A session that follows the default profile writes its final browser state (cookies, storage) back when it ends. The write-back is best-effort: when several default-profile sessions run at once, the first to start becomes the writer and the others load the profile read-only for that round — they never fail over it.
* **Explicit persist is best-effort too.** `persist_browser_profile: true` requests the writer role but never blocks the run: if another session already holds the [write lock](#persisting-state-back-into-the-profile), the session starts read-only instead of failing. For a run that must persist (for example a scheduled login-refresh job), verify the session's effective persist state after creation and reschedule if it was downgraded.

`use_default_browser_profile` is mutually exclusive with `browser_profile_id` — name a profile or follow the default, not both. Auto-created defaults appear in your [profile list](/computer-use-agents/browser-profiles/list) with a `default-<browser>-...` name; deleting one simply means your next default-profile session starts over with a fresh empty default.

### Choosing your own default

If you'd rather your agents follow a profile you curated — for example one [uploaded from a real machine](#creating-a-profile) — promote it with [`PUT /browser-profiles/{profile_id}/default`](/computer-use-agents/browser-profiles/set-default); setting a new default for the same browser replaces the previous one. Check what is currently set with [`GET /browser-profiles/default`](/computer-use-agents/browser-profiles/get-default), and clear it with [`DELETE /browser-profiles/{profile_id}/default`](/computer-use-agents/browser-profiles/unset-default). Profile objects report their status in the `is_default` field. You can only mark profiles you created yourself as your default.
