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

# Handle two-factor authentication

> Let the agent request one-time codes and confirmation links through a built-in tool.

export const TwoFactorAuth = () => {
  const stroke = {
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.75,
    strokeLinecap: "round",
    strokeLinejoin: "round"
  };
  const S = c => ({
    className: c,
    ...stroke
  });
  const icons = {
    agent: c => <svg viewBox="0 0 24 24" {...S(c)}><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2M20 14h2M15 13v2M9 13v2" /></svg>,
    lock: c => <svg viewBox="0 0 24 24" {...S(c)}><rect width="18" height="11" x="3" y="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
  };
  const Card = ({icon, title, sub, children}) => <div className="flex shrink-0 flex-col self-center rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className="flex items-center gap-2.5">
        <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">{icon("h-5 w-5")}</span>
        <div>
          <div className="whitespace-nowrap text-base font-semibold leading-6 text-zinc-900 dark:text-zinc-100">{title}</div>
          <div className="whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">{sub}</div>
        </div>
      </div>
      {children}
    </div>;
  const Arrow = ({top, bottom, dir = "right"}) => <div className="flex flex-col items-stretch justify-center gap-1 px-4 text-center text-xs leading-4 text-zinc-500 dark:text-zinc-400">
      {top && <span className="whitespace-nowrap">{top}</span>}
      <div className={`${dir === "left" ? "flex flex-row-reverse items-center text-zinc-400 dark:text-zinc-600" : "flex items-center text-zinc-400 dark:text-zinc-600"}`}>
        <span className="h-px flex-1 bg-current" />
        <svg className="-ml-px h-3 w-2 shrink-0" viewBox="0 0 8 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={dir === "left" ? {
    transform: "scaleX(-1)",
    marginLeft: 0,
    marginRight: -1
  } : undefined}>
          <path d="M1 1.5 6 6l-5 4.5" />
        </svg>
      </div>
      {bottom && <span className="whitespace-nowrap">{bottom}</span>}
    </div>;
  const Chip = ({children}) => <span className="whitespace-nowrap rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{children}</span>;
  return <div className="not-prose my-8 overflow-x-auto">
      <div className="flex min-w-[620px] items-stretch justify-center">
        <Card icon={icons.agent} title="Agent" sub="login asks for a code">
          <div className="mt-4 flex flex-col items-start gap-2 text-sm text-zinc-600 dark:text-zinc-400">
            <Chip>request_otp</Chip>
            <div>Never guesses; waits for one value</div>
          </div>
        </Card>

        <div className="flex min-w-[200px] flex-1 flex-col justify-center gap-6">
          <Arrow top="prompt, kind, source" />
          <Arrow bottom="code or link" dir="left" />
        </div>

        <Card icon={icons.lock} title="Your handler" sub="runs on your machine">
          <div className="mt-4 flex flex-col items-start gap-2 text-sm text-zinc-600 dark:text-zinc-400">
            <div className="flex gap-1.5">
              <Chip>stdin</Chip>
              <Chip>IMAP inbox</Chip>
              <Chip>custom</Chip>
            </div>
            <div>Inbox and secrets stay local</div>
          </div>
        </Card>
      </div>
    </div>;
};

<TwoFactorAuth />

Email codes, SMS codes, and confirmation links are values the agent can't invent. The SDKs ship a built-in [custom tool](/agents-api/custom-tools) for that moment: the agent calls `request_otp`, your process resolves the code or link, and the run continues with that single value.

Pass `otp_tool` / `otpTool` in `tools` with the handler that matches where the code arrives:

| Where the code arrives | Handler | Best for |
| - | - | - |
| You're at the keyboard | None: prompts in your terminal (Node.js only in TypeScript) | Local runs, debugging |
| An inbox you control | `imap_otp_handler` / `imapOtpHandler` | Unattended runs |
| Slack, an SMS provider, anything else | Your own function, sync or async | Anything your code can reach |
| An authenticator app, secret in 1Password | No tool: a [vault](#authenticator-apps-via-a-vault) | TOTP without exposing the secret |

<CodeGroup>
  ```python Python theme={"system"}
  import os

  from hai_agents import Client
  from hai_agents_tools import OtpRequest, imap_otp_handler, otp_tool

  inbox = imap_otp_handler(
      host="imap.gmail.com",
      username="agent-inbox@gmail.com",
      password=os.environ["GMAIL_APP_PASSWORD"],
      sender="no-reply@example.com",  # optional: only mail from this address
  )

  def from_slack(request: OtpRequest) -> str:
      return slack.ask_user(request.prompt)

  client = Client()
  result = client.run_session(
      agent="h/web-surfer-flash",
      messages="Log in to example.com and check for new notifications.",
      tools=[otp_tool(inbox)],  # or otp_tool() to prompt, otp_tool(from_slack) for your own handler
  )
  print(result.answer)
  ```

  ```typescript TypeScript theme={"system"}
  import { HaiAgentsClient, imapOtpHandler, otpTool, type OtpRequest } from "hai-agents";

  // The IMAP handler needs: npm install imapflow mailparser
  const inbox = imapOtpHandler({
    host: "imap.gmail.com",
    username: "agent-inbox@gmail.com",
    password: process.env.GMAIL_APP_PASSWORD!,
    sender: "no-reply@example.com", // optional: only mail from this address
  });

  async function fromSlack(request: OtpRequest): Promise<string> {
    return slack.askUser(request.prompt);
  }

  const client = new HaiAgentsClient();
  const result = await client.runSession({
    agent: "h/web-surfer-flash",
    messages: "Log in to example.com and check for new notifications.",
    tools: [otpTool({ handler: inbox })], // or otpTool() to prompt, otpTool({ handler: fromSlack })
  });
  console.log(result.answer);
  ```
</CodeGroup>

Python ships the tool in the `hai_agents_tools` module of the `hai-agents` package. TypeScript exports it from `hai-agents`.

## Read the code from email

The IMAP handler polls unread mail (newest first), extracts a code or confirmation link, marks that message read so a retry can't reuse a stale code, and returns only that value. For Gmail or Google Workspace, use an [app password](https://support.google.com/accounts/answer/185833).

It runs in your process: IMAP credentials never leave your machine, and the agent never sees mailbox contents, subjects, or senders.

| Option (Python / TypeScript) | Default | Role |
| - | - | - |
| `host`, `username`, `password` | required | IMAP login |
| `port` | `993` | IMAP over TLS |
| `mailbox` | `"INBOX"` | Mailbox to poll |
| `sender` | unset | Only consider mail from this address |
| `timeout_s` / `timeoutMs` | 2 minutes | Give up (tool error to the agent) after this long |
| `poll_interval_s` / `pollIntervalMs` | 5 seconds | Delay between polls |
| `max_age_s` / `maxAgeMs` | 15 minutes | Ignore unread mail older than this |
| `mark_seen` / `markSeen` | on | Mark the matched message read |
| `code_pattern` / `codePattern` | built-in heuristics | Override extraction. The first capture group (or the whole match) is the code |

## What the agent sends

The tool's input schema is fixed. Your handler receives it as an `OtpRequest`:

| Field | Required | Meaning |
| - | - | - |
| `prompt` | yes | Human-readable ask, e.g. "Enter the 6-digit code sent to j\*\*\*@example.com" |
| `kind` | no | `"code"` (default), or `"link"` for a full confirmation URL |
| `source` | no | Where it was sent, e.g. `"email"`, `"sms"`, `"authenticator app"` |

Return a non-empty string. An empty value fails as a tool error, so the agent can retry or stop cleanly.

## Authenticator apps via a vault

If the site uses a TOTP authenticator and the secret lives in [1Password](https://developer.1password.com/), bind a [vault](/agents-api/vaults/overview) to the browser instead. When the page matches a stored item, the session offers [`fill_secret_at`](/agents-api/browser/configuration#actions) with `totp` and injects the code without putting it in the agent's context.

| | `otp_tool` | Vault |
| - | - | - |
| Code arrives | Out of band: email, SMS, magic link | Generated from a TOTP secret |
| Who resolves it | Your handler, in your process | The session, from 1Password |
| Agent sees | The single code or link | Nothing |


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