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

> ## Agent Instructions
> H Platform has four products: the Agents API (managed computer-use agents, base URL https://agp.eu.hcompany.ai/api/v2 or https://agp.hcompany.ai/api/v2 for the US), the Models API (OpenAI-compatible Holo vision-language models at https://api.hcompany.ai/v1), HoloDesktop CLI (Holo on the user's own desktop), and HoloTab (a free no-code Chrome extension that runs Holo in the user's browser, with recordable routines and schedules).
> Authenticate with a bearer API key from the HAI_API_KEY environment variable. SDKs: `pip install hai-agents` (Python, `from hai_agents import Client`) and `npm install hai-agents` (TypeScript, `import { HaiAgentsClient } from "hai-agents"`). CLI: `hai`.
> Agents do work in a browser or on a desktop; describe the task as an imperative instruction. To run a task quickly, prefer the pre-built agent `h/web-surfer-flash`. Read results from the session's `latest_answer` after it reaches a terminal status.
> Sessions are the unit of work; wait for a terminal status (completed, failed, timed_out, interrupted) before reading the answer. Use webhooks or the `changes` long-poll endpoint to follow progress.

# Browser profiles

> Start cloud browser sessions already signed in: the default profile, persisting state back, and uploading your own profile.

export const Notice = ({kind = "note", title, children}) => {
  const kinds = {
    warning: {
      label: "User notice",
      icon: <>
          <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
          <path d="M12 9v4" />
          <path d="M12 17h.01" />
        </>
    },
    gotcha: {
      label: "Gotcha",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    },
    note: {
      label: "Note",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    }
  };
  const k = kinds[kind];
  return <div className="notice my-6 rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className={`${kind === "warning" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-red-400/80 dark:text-red-400/70" : kind === "gotcha" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-amber-500/80 dark:text-amber-400/70" : "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-500"}`}>
        <svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          {k.icon}
        </svg>
        {k.label}
      </div>
      {title && <div className="not-prose mt-2 text-base font-semibold text-zinc-900 dark:text-zinc-100">{title}</div>}
      <div className="notice-body mt-3 text-sm leading-6 text-zinc-700 dark:text-zinc-300">{children}</div>
    </div>;
};

A **browser profile** is a cloud browser's saved state (cookies, local storage, session data), so the agent starts a session already signed in. Profiles belong to your organization: any agent in it can load one, and they persist until you delete them. Use a [vault](/agents-api/vaults/overview) instead when the agent should sign in with credentials. A profile carries an existing logged-in session forward. A [local browser](/agents-api/local-control) uses its own Chrome profile instead.

## Default profiles

Every user has a **default profile** per browser flavor (for example `chromium`): a browser that remembers you across sessions. You upload nothing and track no ids.

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

* **First use creates it.** If you have no default yet, the session creates an empty profile and marks it as your default when it starts. Sign in to your sites once (for example in the session's live view) and the state carries forward.
* **Sessions save back automatically.** Each session writes its final state back when it ends. Best-effort: when several default-profile sessions run at once, the first to start writes and the others run read-only. They never fail over it.
* **Exclusive with `browser_profile_id`.** Name a profile or follow the default, not both.

Auto-created defaults appear in your [profile list](/agents-api/browser-profiles/list) as `default-<browser>-...`. Delete one and the next default-profile session starts from a fresh empty default.

### Choosing your own default

Promote a profile you curated (for example one you [uploaded](#upload-your-own-profile)). You can only mark profiles you created yourself. Profile objects report their status in `is_default`.

| Action | Endpoint |
| - | - |
| Set (replaces the previous default for that browser) | [`PUT /browser-profiles/{profile_id}/default`](/agents-api/browser-profiles/set-default) |
| Read | [`GET /browser-profiles/default`](/agents-api/browser-profiles/get-default) |
| Clear | [`DELETE /browser-profiles/{profile_id}/default`](/agents-api/browser-profiles/unset-default) |

## Persisting state back into the profile

A profile named with `browser_profile_id` is read-only by default: the session discards whatever the run changes (new cookies, refreshed tokens, dismissed banners), so its logins age out. Set `persist_browser_profile: true` to write the final state back when the session ends.

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

* **One writer at a time.** A persisting session holds an exclusive write lock until it ends. A second session asking to persist the same profile still starts, but read-only. Any number of sessions can load a profile read-only in parallel. For a run that must persist (like a scheduled login-refresh job), check the session's effective persist state after creation and reschedule if it was downgraded.
* **Last write wins.** The session's final state replaces the stored state. It is not merged.
* **Decided at create time.** Request persistence when creating the session, not when attaching to one.
* **Best-effort on stop.** If the write-back fails, the session still stops cleanly and the profile keeps its previous state.

Sessions that follow your [default profile](#default-profiles) persist without the flag.

## Upload your own profile

Upload a profile when you want to bring logins built outside H. You build a Chrome **user-data directory** (the folder holding cookies, local storage, saved logins, and extensions), zip it, and upload it.

<Notice kind="warning" title="Build a dedicated profile, on Linux, on the runner's Chromium">
  * **Never upload your personal Chrome directory** (`~/Library/Application Support/Google/Chrome` on macOS, `~/.config/google-chrome` on Linux). It holds every password, token, and history entry you have. Treat the archive as a secret.
  * **Cookies only carry over from Linux builds.** On macOS and Windows, Chrome always encrypts cookies with the OS keychain (Keychain or DPAPI), so the profile loads logged out. No flag changes that: build in Docker.
  * **Match the runner's Chromium.** Sessions run Chromium 150.0.7871.124. A profile built on a newer version does not load.
  * **Shut Chromium down cleanly.** It writes cookies to disk only on a clean shutdown or an occasional autosave. Killing it (`docker stop` sends `SIGTERM`, `docker rm -f` sends `SIGKILL`) can lose the logins you just made.
</Notice>

### Creating a profile

`--password-store=basic` makes cookies portable: Chrome encrypts them with a built-in key that the session runner also uses. Without it, the agent starts logged out.

<Tabs>
  <Tab title="Docker (any OS)">
    You need [Docker](https://www.docker.com/). The image tag is pinned to the runner's Chromium, so do not use `:latest`.

    ```bash theme={"system"}
    docker run -d --name agent-profile -p 127.0.0.1:3000:3000 \
      -e CHROME_CLI="--password-store=basic" \
      -v "$HOME/agent-profile:/config" \
      lscr.io/linuxserver/chromium:11e602ae-ls45

    # Open http://localhost:3000 and sign in to the sites the agent needs.

    # Flush cookies with SIGINT, wait, then remove the container and zip.
    docker exec agent-profile pkill -INT -o chromium
    sleep 3
    docker rm -f agent-profile
    cd "$HOME/agent-profile/.config/chromium" && zip -r ~/profile.zip .
    ```
  </Tab>

  <Tab title="Linux Chrome">
    Use a Chrome no newer than the runner's. It opens a fresh profile, separate from your everyday one.

    ```bash theme={"system"}
    google-chrome --user-data-dir="$HOME/agent-profile" --password-store=basic

    # Sign in, dismiss cookie banners, install extensions. Quit Chrome cleanly, then:
    cd "$HOME/agent-profile" && zip -r ~/profile.zip .
    ```

    The same steps with desktop Chrome on macOS or Windows keep storage and extensions, but not cookies.
  </Tab>
</Tabs>

### Uploading a profile

The archive goes straight to object storage through a presigned URL, never through the Agents API.

<Steps>
  <Step title="Initiate the upload">
    [`POST /initiate-upload`](/agents-api/browser-profiles/initiate-upload) returns a `profile_id`, a presigned `upload_url`, and the `upload_fields` to send with the file.

    <CodeGroup>
      ```bash cURL theme={"system"}
      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={"system"}
      from hai_agents import Client

      client = Client()

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

      ```typescript TypeScript theme={"system"}
      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` the `.zip` to `upload_url` as `multipart/form-data`, with every entry of `upload_fields`, unmodified. The archive must be `application/zip`, at most **1 GB**. The URL and fields expire after `upload_expires_in` seconds, so upload right away.

    <CodeGroup>
      ```bash cURL theme={"system"}
      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={"system"}
      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={"system"}
      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>
  </Step>

  <Step title="Complete the upload">
    [`POST /{profile_id}/complete-upload`](/agents-api/browser-profiles/complete-upload) with `name`, `browser_name`, and `browser_version` **within 1 day**. The platform verifies the archive and creates the profile. It deletes a pending upload after 1 day.

    <CodeGroup>
      ```bash cURL theme={"system"}
      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": "150"}'
      ```

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

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

Load it with `browser_profile_id` on the [browser environment](/agents-api/browser/configuration). [List, retrieve, or delete](/agents-api/browser-profiles/list) your profiles with the browser profile endpoints.

Related: [Vaults](/agents-api/vaults/overview), [Configure the browser](/agents-api/browser/configuration).


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