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

# Credential fill

> Type stored logins and one-time codes into a session's login form, without your code or an LLM seeing the values.

Credential fill types stored [secrets](/docs/sandboxes/secrets), such as a username, a password or a one-time code, into a login form in a browser session. Values are encrypted on your machine before they're uploaded, and the API never returns them. When your session reaches a login page, you ask Hyperbrowser to fill the form: the values are decrypted on the host running the session's browser and typed into the inputs you choose. Your code, your logs and any LLM driving the browser refer to the secrets by name only. If your logins live in 1Password, a session can also [fill fields read from 1Password](#fill-from-1password) without storing them.

<Note>
  Credential fill is in public preview and is enabled for teams on request. Please contact us at [info@hyperbrowser.ai](mailto:info@hyperbrowser.ai) or on our support chat to enable it for your team. A restricted API key needs `secrets.manage` to create secrets and vaults, `secrets.use` to attach them to a session, and `secrets.use` with `browser.sessions.update` to fill them. It's available in the Node SDK from 1.3.0, the Python SDK from 1.13.0, and the [CLI](/docs/sandboxes/cli).
</Note>

## Store a login

A login is a few ordinary secrets, one per input, each with the **allowed origins** it may be typed into. A secret can't be filled until it has at least one. To fill a one-time code, store the authenticator seed as a secret of type `totp`: each fill types the code that's current at that moment.

Group the secrets in a [vault](/docs/sandboxes/vaults) so you can attach and fill them by one name. The vault's entry names, such as `USERNAME` and `PASSWORD`, are the field names you fill by.

<CodeGroup>
  ```typescript Node.js theme={null}
  import { Hyperbrowser } from "@hyperbrowser/sdk";

  const client = new Hyperbrowser({ apiKey: process.env.HYPERBROWSER_API_KEY });
  const allowedOrigins = ["https://github.com"];

  await client.secrets.create({ name: "github-username", value: process.env.GITHUB_USERNAME!, allowedOrigins });
  await client.secrets.create({ name: "github-password", value: process.env.GITHUB_PASSWORD!, allowedOrigins });
  await client.secrets.create({
    name: "github-otp",
    type: "totp",
    value: process.env.GITHUB_TOTP_SEED!,
    allowedOrigins,
  });

  await client.vaults.create({
    name: "github-login",
    entries: {
      USERNAME: { name: "github-username" },
      PASSWORD: { name: "github-password" },
      OTP: { name: "github-otp" },
    },
  });
  ```

  ```python Python theme={null}
  import os
  from hyperbrowser import Hyperbrowser

  client = Hyperbrowser(api_key=os.environ["HYPERBROWSER_API_KEY"])
  allowed_origins = ["https://github.com"]

  client.secrets.create({"name": "github-username", "value": os.environ["GITHUB_USERNAME"], "allowed_origins": allowed_origins})
  client.secrets.create({"name": "github-password", "value": os.environ["GITHUB_PASSWORD"], "allowed_origins": allowed_origins})
  client.secrets.create({
      "name": "github-otp",
      "type": "totp",
      "value": os.environ["GITHUB_TOTP_SEED"],
      "allowed_origins": allowed_origins,
  })

  client.vaults.create({
      "name": "github-login",
      "entries": {
          "USERNAME": {"name": "github-username"},
          "PASSWORD": {"name": "github-password"},
          "OTP": {"name": "github-otp"},
      },
  })
  ```

  ```bash CLI theme={null}
  hx secrets create github-username --allowed-origin https://github.com --from-file ./username.txt
  hx secrets create github-password --allowed-origin https://github.com --from-file ./password.txt
  hx secrets create github-otp --type totp --allowed-origin https://github.com --from-file ./otp-seed.txt

  hx vaults create github-login \
    --entry USERNAME=github-username --entry PASSWORD=github-password --entry OTP=github-otp
  ```
</CodeGroup>

An allowed origin is `scheme://host[:port]`, with no path. `https://*.example.com` matches any subdomain of `example.com` but not `example.com` itself, and `http://` is accepted only for `localhost`, `127.0.0.1` and `[::1]`. A secret takes 1–20 origins. Allowed origins only limit fills: the same secret can still be passed to a [sandbox](/docs/sandboxes/secrets), where a process that names it, or a vault holding it, receives the value, or a `totp` secret's current code.

A TOTP value is the authenticator's base32 seed, or an `otpauth://totp/...` URI when the site uses a non-default algorithm, digit count or period. Spaces, hyphens, line breaks and lowercase letters in a seed are fine. The SDKs and CLI check the value before uploading it, because the API can't see it: an invalid seed only fails when it's used.

To change a secret's origins later, see [Manage logins](#manage-logins).

## Fill a login form

Attach the vault, or single secrets, when you create the session; a session can only fill what it was created with. Then navigate to the login page and fill it. These examples use Playwright: install `playwright-core` (Node.js) or `playwright` (Python).

<CodeGroup>
  ```typescript Node.js theme={null}
  import { Hyperbrowser } from "@hyperbrowser/sdk";
  import { chromium } from "playwright-core";

  const client = new Hyperbrowser({ apiKey: process.env.HYPERBROWSER_API_KEY });

  const session = await client.sessions.create({ credentials: [{ vault: "github-login" }] });
  try {
    const browser = await chromium.connectOverCDP(session.wsEndpoint);
    const page = browser.contexts()[0].pages()[0];
    await page.goto("https://github.com/login");

    const result = await client.sessions.credentials.fill(session.id, {
      vault: "github-login",
      fields: [
        { field: "USERNAME", selector: "#login_field" },
        { field: "PASSWORD", selector: "#password" },
      ],
    });
    console.log(result.origin, result.fields);

    // Fill only types. Submitting is up to you.
    if (result.fields.every((field) => field.outcome === "filled")) {
      await page.click("input[type=submit]");
    }
  } finally {
    await client.sessions.stop(session.id);
  }
  ```

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

  client = Hyperbrowser(api_key=os.environ["HYPERBROWSER_API_KEY"])

  session = client.sessions.create({"credentials": [{"vault": "github-login"}]})
  try:
      with sync_playwright() as p:
          browser = p.chromium.connect_over_cdp(session.ws_endpoint)
          page = browser.contexts[0].pages[0]
          page.goto("https://github.com/login")

          result = client.sessions.credentials.fill(session.id, {
              "vault": "github-login",
              "fields": [
                  {"field": "USERNAME", "selector": "#login_field"},
                  {"field": "PASSWORD", "selector": "#password"},
              ],
          })
          print(result.origin, result.fields)

          # Fill only types. Submitting is up to you.
          if all(field.outcome == "filled" for field in result.fields):
              page.click("input[type=submit]")
  finally:
      client.sessions.stop(session.id)
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.hyperbrowser.ai/api/session \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"credentials": [{"vault": "github-login"}]}'

  # After navigating the session to https://github.com/login:
  curl -X POST https://api.hyperbrowser.ai/api/session/SESSION_ID/credentials/fill \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "vault": "github-login",
      "fields": [
        {"field": "USERNAME", "selector": "#login_field"},
        {"field": "PASSWORD", "selector": "#password"}
      ]
    }'
  ```

  ```bash CLI theme={null}
  hx browser create --vault github-login
  hx browser fill <session-id> --vault github-login \
    --field USERNAME=#login_field --field PASSWORD=#password
  ```
</CodeGroup>

Each field names a vault entry and the CSS selector of its input. To fill one secret instead, pass `credential` with the secret's name or ID and give each field only a `selector`:

<CodeGroup>
  ```typescript Node.js theme={null}
  await client.sessions.credentials.fill(session.id, {
    credential: "github-password",
    fields: [{ selector: "#password" }],
  });
  ```

  ```python Python theme={null}
  client.sessions.credentials.fill(session.id, {
      "credential": "github-password",
      "fields": [{"selector": "#password"}],
  })
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.hyperbrowser.ai/api/session/SESSION_ID/credentials/fill \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"credential": "github-password", "fields": [{"selector": "#password"}]}'
  ```

  ```bash CLI theme={null}
  hx browser fill <session-id> --credential github-password --selector '#password'
  ```
</CodeGroup>

The session's `credentials` list takes up to 20 items: a secret's name, `{ secret: name }`, `{ vault: name }`, or `{ onePassword: name }` for a [1Password token](#fill-from-1password). A vault attaches the secrets its entries point at when the session is created. Attaching anything forces a Firecracker browser and turns off Chrome's password manager. Each fill passes exactly one of `credential`, `vault` or `onePassword`.

### One-time codes

Logins with two-step verification usually ask for the code on the next screen. Submit the first screen, then fill the `totp` entry there:

<CodeGroup>
  ```typescript Node.js theme={null}
  await page.waitForSelector("#app_totp");
  const otp = await client.sessions.credentials.fill(session.id, {
    vault: "github-login",
    fields: [{ field: "OTP", selector: "#app_totp" }],
  });
  console.log(otp.fields[0].expiresAt);
  ```

  ```python Python theme={null}
  page.wait_for_selector("#app_totp")
  otp = client.sessions.credentials.fill(session.id, {
      "vault": "github-login",
      "fields": [{"field": "OTP", "selector": "#app_totp"}],
  })
  print(otp.fields[0].expires_at)
  ```
</CodeGroup>

The field's `expiresAt` says when the typed code stops being current. If you're about to submit after that, fill it again.

## Fill from 1Password

A session can type fields read from your [1Password](/docs/sandboxes/1password) account, so you don't have to store the login in Hyperbrowser. Store a service account token as an [`op_token` secret](/docs/sandboxes/1password#store-the-token), and attach it when you create the session with `{ onePassword: "op-token" }` in `credentials`. Attaching the token lets fills read with it; the token itself is never typed. Naming an `op_token` as a plain credential, `"op-token"` or `{ secret: "op-token" }`, fails with `SECRET_TYPE_MISMATCH`.

To fill, pass `onePassword` with the token's name, the 1Password vault's name or ID, and the `allowedOrigins` the values may be typed on. Each field gives the input's `selector` and, in `ref`, the item, an optional section and the field to type:

<CodeGroup>
  ```typescript Node.js theme={null}
  const session = await client.sessions.create({ credentials: [{ onePassword: "op-token" }] });

  // After navigating the session to https://www.scrapingcourse.com/login:
  const result = await client.sessions.credentials.fill(session.id, {
    onePassword: {
      token: "op-token",
      vault: "Dev Vault for Testing",
      allowedOrigins: ["https://www.scrapingcourse.com"],
    },
    fields: [
      { selector: "#email", ref: { item: "scrapingcourse login", field: "username" } },
      { selector: "#password", ref: { item: "scrapingcourse login", field: "password" } },
    ],
  });
  console.log(result.fields.map((field) => [field.reference, field.outcome]));
  ```

  ```python Python theme={null}
  session = client.sessions.create({"credentials": [{"one_password": "op-token"}]})

  # After navigating the session to https://www.scrapingcourse.com/login:
  result = client.sessions.credentials.fill(session.id, {
      "one_password": {
          "token": "op-token",
          "vault": "Dev Vault for Testing",
          "allowed_origins": ["https://www.scrapingcourse.com"],
      },
      "fields": [
          {"selector": "#email", "ref": {"item": "scrapingcourse login", "field": "username"}},
          {"selector": "#password", "ref": {"item": "scrapingcourse login", "field": "password"}},
      ],
  })
  print([(field.reference, field.outcome) for field in result.fields])
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.hyperbrowser.ai/api/session \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"credentials": [{"onePassword": "op-token"}]}'

  # After navigating the session to https://www.scrapingcourse.com/login:
  curl -X POST https://api.hyperbrowser.ai/api/session/SESSION_ID/credentials/fill \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "onePassword": {
        "token": "op-token",
        "vault": "Dev Vault for Testing",
        "allowedOrigins": ["https://www.scrapingcourse.com"]
      },
      "fields": [
        {"selector": "#email", "ref": {"item": "scrapingcourse login", "field": "username"}},
        {"selector": "#password", "ref": {"item": "scrapingcourse login", "field": "password"}}
      ]
    }'
  ```

  ```bash CLI theme={null}
  hx browser create --1password-token op-token
  hx browser fill <session-id> --1password-token op-token --1password-vault "Dev Vault for Testing" \
    --allowed-origin https://www.scrapingcourse.com \
    --1password-ref "scrapingcourse login/username" --selector '#email' \
    --1password-ref "scrapingcourse login/password" --selector '#password'
  ```
</CodeGroup>

In the CLI, `--1password-ref` takes `item/[section/]field`, and the nth `--1password-ref` fills the nth `--selector`.

`allowedOrigins` is required: 1–20 origins with the same rules as a [secret's allowed origins](#store-a-login). An origin that breaks them fails the fill with `SECRET_METADATA_INVALID`. A stored secret carries its own origins, but nothing stored in Hyperbrowser says where a 1Password value belongs, so the request does. The browser checks the target page against them exactly as it does for stored secrets, and [tab choice](#choose-the-page-and-frame) uses them in the same way.

Each `ref` follows the [1Password reference rules](/docs/sandboxes/1password#references) and reads `op://<vault>/<item>/[<section>/]<field>`. To type a one-time code, add `?attribute=otp` to its field, such as `{ item: "scrapingcourse login", field: "one-time password?attribute=otp" }`. A fill uses one token and one 1Password vault, every field needs a `ref`, and `ref` can't be used with `credential` or `vault`. Each fill reads the fields from 1Password at that moment, with the token's latest version.

The [result](#results) has each field's `reference`, and the `secretId` and `version` of the token it was read with. It has no `field` or `expiresAt`, and no top-level `version`:

```json theme={null}
{
  "pageId": "6C1F0E5A9B2D47E8A1F3C2B4D5E6F708",
  "origin": "https://www.scrapingcourse.com",
  "fields": [
    { "selector": "#email", "outcome": "filled", "secretId": "…", "version": 2,
      "reference": "op://Dev Vault for Testing/scrapingcourse login/username" },
    { "selector": "#password", "outcome": "filled", "secretId": "…", "version": 2,
      "reference": "op://Dev Vault for Testing/scrapingcourse login/password" }
  ]
}
```

The [event log](#how-credentials-are-protected) records the token's name as `onePasswordToken`, with its `secretId`, where other fills record `credential` or `vault`. The [LLM tool](#use-with-an-llm) doesn't fill from 1Password, so call fill from your code. Besides the [errors](#errors) of any fill, you can get:

| Code | Status | Meaning |
| - | - | - |
| `CREDENTIAL_NOT_ATTACHED` | 403 | The token wasn't attached with `{ onePassword }` when the session was created. |
| `SECRET_METADATA_INVALID` | 400 | An allowed origin breaks the rules. |
| `SECRET_ONEPASSWORD_REFERENCE_FAILED` | 422 | 1Password couldn't resolve a field; `details.selectors` and `details.reasons` (by selector) name the inputs. Nothing was typed. |
| `SECRET_ONEPASSWORD_AUTH_FAILED` | 422 | 1Password rejected the token, or the token is malformed or signs in outside `1password.com`, `1password.ca` and `1password.eu` (or their subdomains), so it wasn't sent. Add a new version of the `op_token` secret. |
| `SECRET_ONEPASSWORD_RATE_LIMITED` | 429 | 1Password is rate limiting the service account. Retryable. |
| `SECRET_ONEPASSWORD_FAILED` | 502, 503 | 1Password couldn't be reached. Retryable. |

The `SECRET_ONEPASSWORD_*` errors name the token version they used in `details.tokenSecretId` and `details.tokenVersion`.

## Results

The response reports an outcome per field, in request order, and never contains values:

```json theme={null}
{
  "pageId": "6C1F0E5A9B2D47E8A1F3C2B4D5E6F708",
  "origin": "https://github.com",
  "fields": [
    { "field": "USERNAME", "selector": "#login_field", "outcome": "filled", "secretId": "…", "version": 1 },
    { "field": "PASSWORD", "selector": "#password", "outcome": "filled", "secretId": "…", "version": 3 }
  ]
}
```

| Outcome | Meaning |
| - | - |
| `filled` | The input holds exactly the stored value. |
| `not_found` | No input matched the selector. |
| `ambiguous` | More than one visible input matched the selector. |
| `not_editable` | The input is disabled, read-only, hidden, or not a text input. |
| `not_accepted` | The page didn't keep the value, for example because of a `maxlength`. |

Each field also reports the `secretId` and `version` typed, and `expiresAt` for a TOTP code. A single-secret fill also has a top-level `version`, and a [1Password fill](#fill-from-1password) reports each field's `reference`. Only inputs a person could see count toward a selector, so a hidden copy of a form, such as a mobile layout or a honeypot field, is skipped. A fill takes 1–16 fields, each selector at most once. It never submits the form and is safe to retry. For logins split over several screens, fill the fields on the current screen, submit, and fill the rest on the next one.

## Choose the page and frame

Without a page ID, the fill chooses a tab like this:

1. If you pass `pageUrl`, only tabs whose URL starts with it are considered.
2. If one tab is left, that's the target.
3. If several are left, it prefers the tabs on an origin that every secret being filled allows, or that the request's `allowedOrigins` allow for 1Password, if any are open, and then the visible tab among them. With `frameUrl`, tabs aren't narrowed by origin, since a login iframe's tab is usually on another origin.
4. If more than one tab still qualifies, the fill fails with `CREDENTIAL_PAGE_AMBIGUOUS`, and the error's details list each candidate's `pageId`, `url` and `title`.

The origin of the chosen tab, or of the frame inside it, is then checked against the allowed origins. If it isn't allowed, nothing is typed and the fill fails with `CREDENTIAL_ORIGIN_NOT_ALLOWED`. To choose the tab yourself, pass `pageId` (`page_id` in Python), the tab's CDP target ID, or a longer `pageUrl` (`page_url`). You can't pass both.

Under Playwright every tab reports as visible, so pass a page when more than one open tab is on an allowed origin:

<CodeGroup>
  ```typescript Node.js theme={null}
  const cdp = await page.context().newCDPSession(page);
  const { targetInfo } = await cdp.send("Target.getTargetInfo");

  await client.sessions.credentials.fill(session.id, {
    vault: "github-login",
    pageId: targetInfo.targetId,
    fields: [{ field: "PASSWORD", selector: "#password" }],
  });
  ```

  ```python Python theme={null}
  client.sessions.credentials.fill(session.id, {
      "vault": "github-login",
      "page_url": "https://github.com/login",
      "fields": [{"field": "PASSWORD", "selector": "#password"}],
  })
  ```
</CodeGroup>

For a login form inside an iframe, pass `frameUrl` (`frame_url` in Python; `frame: { "url": … }` in the API) with the start of the iframe's URL, such as `https://accounts.example.com/login`. The iframe's own origin must be allowed.

## Use with an LLM

Both SDKs include a `fill_credential` tool definition for OpenAI, Anthropic and Gemini, and a runner that executes the model's call in the session you bind. The model chooses a secret (`credential`) or a vault, the field and selector for each input, and optionally `page_id`, `page_url` or `frame_url`; it never sees the values. The runner returns JSON text for the model: the fill result, or `{"error": {"code", "message", "retryable", "details"}}` so the model can react, for example by retrying with a `page_id` from `details.pages`.

The tool doesn't offer 1Password. A 1Password fill names its allowed origins in the request, so a model choosing them would choose where your values may be typed. For 1Password, [call fill](#fill-from-1password) from your code.

Give the model your other browser tools as well, since it has to submit the form itself.

<CodeGroup>
  ```python Python (OpenAI) theme={null}
  import json
  from openai import OpenAI
  from hyperbrowser.tools import CredentialFillTool

  openai_client = OpenAI()
  tools = [CredentialFillTool.openai_tool_definition]  # plus your browser tools
  messages = [{"role": "user", "content": "Sign in to GitHub with the vault named github-login."}]

  while True:
      response = openai_client.chat.completions.create(model="gpt-5", messages=messages, tools=tools)
      message = response.choices[0].message
      messages.append(message)
      if not message.tool_calls:
          break
      for call in message.tool_calls:
          if call.function.name == "fill_credential":
              output = CredentialFillTool.runnable(client, session.id, json.loads(call.function.arguments))
          else:
              output = run_browser_tool(call)  # your click and navigate tools
          messages.append({"role": "tool", "tool_call_id": call.id, "content": output})
  ```

  ```typescript Node.js (Gemini) theme={null}
  import { GoogleGenAI } from "@google/genai";
  import { CredentialFillTool } from "@hyperbrowser/sdk/tools";

  const ai = new GoogleGenAI({});
  const config = { tools: [{ functionDeclarations: [CredentialFillTool.geminiToolDefinition] }] };
  const contents: any[] = [{ role: "user", parts: [{ text: "Sign in to GitHub with the vault named github-login." }] }];

  while (true) {
    const response = await ai.models.generateContent({ model: "gemini-3.5-flash", contents, config });
    contents.push(response.candidates![0].content);
    if (!response.functionCalls?.length) break;
    const parts = [];
    for (const call of response.functionCalls) {
      const output = await CredentialFillTool.runnable(client, session.id, call.args as any);
      parts.push({ functionResponse: { id: call.id, name: call.name, response: { result: JSON.parse(output) } } });
    }
    contents.push({ role: "user", parts });
  }
  ```
</CodeGroup>

The definitions are `openai_tool_definition`, `anthropic_tool_definition` and `gemini_tool_definition` in Python, and `openaiToolDefinition`, `anthropicToolDefinition` and `geminiToolDefinition` in Node.js. Python also has `CredentialFillTool.async_runnable` for `AsyncHyperbrowser`.

## Manage logins

Login secrets are ordinary secrets, so you manage them with the secrets methods; see [Secrets](/docs/sandboxes/secrets#rotate-and-delete) to rotate values and [Vaults](/docs/sandboxes/vaults) to change entries. Each method takes a name or ID.

<CodeGroup>
  ```typescript Node.js theme={null}
  // Replace the allowed origins, or clear them with null.
  await client.secrets.setAllowedOrigins("github-password", ["https://github.com", "https://gist.github.com"]);

  // Store a new value; fills use the latest version.
  await client.secrets.addVersion("github-password", newPassword);
  ```

  ```python Python theme={null}
  # Replace the allowed origins, or clear them with None.
  client.secrets.set_allowed_origins("github-password", ["https://github.com", "https://gist.github.com"])

  # Store a new value; fills use the latest version.
  client.secrets.add_version("github-password", new_password)
  ```

  ```bash cURL theme={null}
  curl -X PATCH https://api.hyperbrowser.ai/api/secrets/SECRET_ID \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"metadata": {"browser": {"allowedOrigins": ["https://github.com", "https://gist.github.com"]}}}'
  ```

  ```bash CLI theme={null}
  hx secrets update github-password --allowed-origin https://github.com --allowed-origin https://gist.github.com
  hx secrets set github-password --from-file ./password.txt
  ```
</CodeGroup>

The API addresses secrets by ID; `{"metadata": {"browser": null}}` clears the origins, after which the secret can't be filled.

## How credentials are protected

* **Encrypted before upload.** Values are encrypted on your machine and decrypted only by the host running the session that fills them. 1Password values are read by that host at fill time and never stored. No endpoint returns a value, and fill results contain outcomes only.
* **Typed only on allowed origins.** Before anything is typed, the origin the browser reports for the target page or frame is checked against the allowed origins of every secret being filled, or for 1Password against the request's `allowedOrigins`, since nothing stored says where a 1Password value belongs. On any other origin the fill fails with `CREDENTIAL_ORIGIN_NOT_ALLOWED` and nothing is typed. Allowed origins don't apply to sandboxes, which receive any secret a process names; keep a login's secrets out of vaults used with sandboxes if only browsers should have them.
* **Attached up front.** A session can only fill secrets, or read with 1Password tokens, attached when it was created. Attaching forces a Firecracker browser and turns off Chrome's password manager.
* **Never submitted for you.** Fill only types. Your code or model decides whether to submit.
* **Kept out of replays.** Inputs a fill writes to are excluded from session replays.
* **Tracked through snapshots.** A session that received a credential, and its snapshots, are marked as containing secrets. Restoring such a snapshot needs the same access as using credentials.
* **Recorded.** Every fill and every refused attempt is written to the session's event log as `credential_filled` or `credential_fill_refused`, with the secret IDs and versions, never values. A 1Password fill records the token's name as `onePasswordToken`, and each field's `reference`. A refusal's `metadata.reason` is the error code.

```bash cURL theme={null}
curl "https://api.hyperbrowser.ai/api/session/SESSION_ID/event-logs?types=credential_filled&types=credential_fill_refused" \
  -H "x-api-key: YOUR_API_KEY"
```

Some things stay readable. Once a value is in a page, the site's scripts, browser extensions, and your own CDP connection to the session can read the field. Video recordings and the live view show what's on screen: password inputs are masked by the page, but a filled username or one-time code is visible.

## Errors

Errors have a `code`, a `message`, and sometimes `retryable` and `details`.

| Code | Status | Meaning |
| - | - | - |
| `CREDENTIAL_ORIGIN_NOT_ALLOWED` | 403 | The page's origin isn't allowed; `details.origin` names it. Nothing was typed. |
| `CREDENTIAL_NOT_ATTACHED` | 403 | The secret or 1Password token wasn't attached when the session was created, or a vault entry now points at a different secret. |
| `CREDENTIAL_NOT_READY` | 409 | The secret has no allowed origins, or no value. |
| `CREDENTIAL_PAGE_AMBIGUOUS` | 409 | Several tabs could be the target; pick one from `details.pages`. |
| `CREDENTIAL_FRAME_AMBIGUOUS` | 409 | Several frames match the frame URL; make it longer. |
| `CREDENTIAL_PAGE_NOT_FOUND`, `CREDENTIAL_FRAME_NOT_FOUND` | 404 | No such tab, or no frame with that URL. |
| `SECRET_NOT_FOUND`, `VAULT_NOT_FOUND`, `VAULT_ENTRY_NOT_FOUND` | 404 | No secret, vault or vault entry with that name. |
| `SECRET_VERSION_NOT_FOUND` | 404 | The secret version to use doesn't exist, such as a 1Password token with no value. |
| `SECRET_TYPE_MISMATCH` | 409 | The secret can't be filled, such as an `op_token` named as a credential, or `onePassword` names a secret that isn't an `op_token`. |
| `SECRET_SOURCES_INVALID` | 400 | The vault attached at session create has no entries. |
| `SECRET_VALUE_INVALID` | 422 | The stored value can't be used, such as an invalid TOTP seed. Add a new version. |
| `SESSION_NOT_ACTIVE` | 409 | The session has stopped. |
| `CREDENTIAL_FILL_TIMEOUT` | 503, 504 | The fill didn't complete in time. Retryable. |
| `CREDENTIAL_FILL_FAILED` | 500, 502 | The browser couldn't complete the fill. |
| `CREDENTIALS_UNSUPPORTED_RUNTIME`, `CREDENTIAL_FILL_UNSUPPORTED` | 400, 501 | The session's browser can't fill credentials. |
| `SECRET_METADATA_INVALID`, `SECRET_ONEPASSWORD_*` | 400, 422, 429, 502, 503 | A 1Password fill failed; see [Fill from 1Password](#fill-from-1password). |
| `CREDENTIALS_SNAPSHOT_PASSWORD_MANAGER_ENABLED` | 400 | The session restores a snapshot taken with Chrome's password manager on. |
| `SECRETS_FEATURE_DISABLED` | 403 | Secrets aren't enabled for your team yet. Please contact support to enable them. |
| `SECRETS_NOT_CONFIGURED` | 503 | The session's host can't decrypt secrets. Nothing was typed. |

A fill can take up to about 18 seconds, so give your HTTP client a timeout of at least 30 seconds. The SDKs do this for you.


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