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

# Document OCR

> Turn a digitally rendered page into Markdown in one chat completion: headings, tables, equations, reading order.

export const OcrDemo = () => {
  const SRC = "/images/models-api/attention-page-1.webp";
  const RED = "#e11d48";
  const blocks = [{
    box: [33.96, 12.31, 31.99, 2.77],
    md: "Attention Is All You Need"
  }, {
    box: [18.47, 23.0, 17.44, 4.85],
    md: "Ashish Vaswani*  \nGoogle Brain  \navaswani@google.com"
  }, {
    box: [37.09, 23.0, 14.02, 4.85],
    md: "Noam Shazeer*  \nGoogle Brain  \nnoam@google.com"
  }, {
    box: [52.31, 23.0, 14.87, 4.85],
    md: "Niki Parmar*  \nGoogle Research  \nnikip@google.com"
  }, {
    box: [68.37, 23.0, 13.47, 4.85],
    md: "Jakob Uszkoreit*  \nGoogle Research  \nusz@google.com"
  }, {
    box: [20.13, 29.31, 14.87, 4.85],
    md: "Llion Jones*  \nGoogle Research  \nllion@google.com"
  }, {
    box: [37.87, 29.31, 18.29, 4.85],
    md: "Aidan N. Gomez* †  \nUniversity of Toronto  \naidan@cs.toronto.edu"
  }, {
    box: [59.02, 29.31, 20.86, 4.85],
    md: "Łukasz Kaiser*  \nGoogle Brain  \nlukaszkaiser@google.com"
  }, {
    box: [38.29, 35.63, 23.42, 3.47],
    md: "Illia Polosukhin* ‡  \nillia.polosukhin@gmail.com"
  }, {
    box: [45.77, 42.2, 8.47, 2.11],
    md: "Abstract"
  }, {
    box: [22.91, 45.41, 54.46, 21.15],
    md: "The dominant sequence transduction models are based on complex recurrent or convolutional neural networks that include an encoder and a decoder. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely. Experiments on two machine translation tasks show these models to be superior in quality while being more parallelizable and requiring significantly less time to train. Our model achieves 28.4 BLEU on the WMT 2014 English-to-German translation task, improving over the existing best results, including ensembles, by over 2 BLEU. On the WMT 2014 English-to-French translation task, our model establishes a new single-model state-of-the-art BLEU score of 41.8 after training for 3.5 days on eight GPUs, a small fraction of the training costs of the best models from the literature. We show that the Transformer generalizes well to other tasks by applying it successfully to English constituency parsing both with large and limited training data."
  }, {
    box: [17.05, 68.79, 14.73, 2.11],
    md: "1 Introduction"
  }, {
    box: [17.05, 71.93, 65.91, 3.25],
    md: "Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neural networks in particular, have been firmly established as state of the art approaches in sequence modeling and"
  }, {
    box: [17.05, 75.51, 65.96, 11.97],
    md: "*Equal contribution. Listing order is random. Jakob proposed replacing RNNs with self-attention and started the effort to evaluate this idea. Ashish, with Illia, designed and implemented the first Transformer models and has been crucially involved in every aspect of this work. Noam proposed scaled dot-product attention, multi-head attention and the parameter-free position representation and became the other person involved in nearly every detail. Niki designed, implemented, tuned and evaluated countless model variants in our original codebase and tensor2tensor. Llion also experimented with novel model variants, was responsible for our initial codebase, and efficient inference and visualizations. Łukasz and Aidan spent countless long days designing various parts of and implementing tensor2tensor, replacing our earlier codebase, greatly improving results and massively accelerating our research."
  }, {
    box: [19.68, 87.18, 24.6, 1.73],
    md: "†Work performed while at Google Brain."
  }, {
    box: [19.68, 88.56, 26.71, 1.73],
    md: "‡Work performed while at Google Research."
  }, {
    box: [17.05, 92.26, 58.74, 1.73],
    md: "31st Conference on Neural Information Processing Systems (NIPS 2017), Long Beach, CA, USA."
  }];
  const [i, setI] = useState(0);
  const refs = useRef([]);
  const pick = (k, scroll) => {
    setI(k);
    if (scroll && refs.current[k]) refs.current[k].scrollIntoView({
      block: "nearest",
      behavior: "smooth"
    });
  };
  const b = blocks[i].box;
  return <div className="not-prose my-6 overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
      <div className="grid" style={{
    gridTemplateColumns: "minmax(0, 5fr) minmax(0, 7fr)"
  }}>
        <div className="border-b border-r border-zinc-200 px-5 py-3 text-sm font-medium text-zinc-700 dark:border-zinc-800 dark:text-zinc-200">Page image in</div>
        <div className="border-b border-zinc-200 px-5 py-3 text-sm font-medium text-zinc-700 dark:border-zinc-800 dark:text-zinc-200">Markdown out <span className="font-normal text-zinc-400 dark:text-zinc-500">· hover a block</span></div>

        <div className="border-r border-zinc-200 p-4 dark:border-zinc-800">
          <div className="relative overflow-hidden rounded-md border border-zinc-200 dark:border-zinc-700">
            <img src={SRC} alt="First page of Attention Is All You Need" className="block w-full" />
            {blocks.map((bl, k) => <button key={k} type="button" aria-label={`Block ${k + 1}`} onMouseEnter={() => pick(k, true)} onClick={() => pick(k, true)} className="absolute" style={{
    left: `${bl.box[0]}%`,
    top: `${bl.box[1]}%`,
    width: `${bl.box[2]}%`,
    height: `${bl.box[3]}%`
  }} />)}
            <span className="pointer-events-none absolute rounded-sm" style={{
    left: `${b[0]}%`,
    top: `${b[1]}%`,
    width: `${b[2]}%`,
    height: `${b[3]}%`,
    border: `1px solid ${RED}`,
    background: "rgba(225, 29, 72, 0.08)",
    transition: "all 150ms"
  }} />
          </div>
        </div>

        <div className="relative">
          <div className="absolute inset-0 overflow-y-auto p-4">
            {blocks.map((bl, k) => <div key={k} ref={el => refs.current[k] = el} onMouseEnter={() => setI(k)} onClick={() => setI(k)} className={`${k === i ? "cursor-default whitespace-pre-wrap rounded-md px-2 py-1.5 font-mono text-xs leading-relaxed transition-colors bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50" : "cursor-default whitespace-pre-wrap rounded-md px-2 py-1.5 font-mono text-xs leading-relaxed transition-colors text-zinc-500 dark:text-zinc-400"}`}>
                {bl.md}
              </div>)}
          </div>
        </div>
      </div>

      <div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 border-t border-zinc-200 px-5 py-3 text-sm text-zinc-500 dark:border-zinc-800 dark:text-zinc-400">
        <span>
          <a href="https://arxiv.org/abs/1706.03762v5" target="_blank" rel="noreferrer" className="underline decoration-zinc-300 underline-offset-2 hover:text-zinc-800 dark:decoration-zinc-600 dark:hover:text-zinc-200">arXiv:1706.03762</a> · page 1 · 300 DPI
        </span>
        <span>Holo3.1 35B-A3B · 3,728 tokens in · 624 out · 5.2 s</span>
      </div>
    </div>;
};

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>;
};

<OcrDemo />

## Transcribe a page

No dedicated endpoint: the same OpenAI-compatible `chat/completions` call, with a page image and a transcription prompt, sent at `temperature=0.0` with `enable_thinking=False`. The Markdown comes back in `message.content`. Client setup is in the [Quickstart](/models-api/quickstart).

<CodeGroup>
  ```python Python theme={"system"}
  IMAGE_URL = "https://your-host/page.png"  # or "data:image/png;base64,..."

  OCR_PROMPT = (
      "Transcribe this document page to Markdown, preserving the reading order, "
      "headings, lists, and tables. Render tables as Markdown tables and equations "
      "as LaTeX. Return only the transcription, with no commentary and no surrounding "
      "code fence. If the page has no readable text, return an empty string."
  )

  response = client.chat.completions.create(
      model="holo3-1-35b-a3b",
      messages=[{
          "role": "user",
          "content": [
              {"type": "image_url", "image_url": {"url": IMAGE_URL}},
              {"type": "text", "text": OCR_PROMPT},
          ],
      }],
      temperature=0.0,
      extra_body={"chat_template_kwargs": {"enable_thinking": False}},
  )

  print(response.choices[0].message.content)
  ```

  ```typescript TypeScript theme={"system"}
  const IMAGE_URL = "https://your-host/page.png"; // or "data:image/png;base64,..."

  const OCR_PROMPT =
    "Transcribe this document page to Markdown, preserving the reading order, " +
    "headings, lists, and tables. Render tables as Markdown tables and equations " +
    "as LaTeX. Return only the transcription, with no commentary and no surrounding " +
    "code fence. If the page has no readable text, return an empty string.";

  const response = await client.chat.completions.create({
    model: "holo3-1-35b-a3b",
    messages: [
      {
        role: "user",
        content: [
          { type: "image_url", image_url: { url: IMAGE_URL } },
          { type: "text", text: OCR_PROMPT },
        ],
      },
    ],
    temperature: 0.0,
    // chat_template_kwargs is H-specific, passed through in the request body
    ...({ chat_template_kwargs: { enable_thinking: false } } as any),
  });

  console.log(response.choices[0].message.content);
  ```
</CodeGroup>

| Input                                                                   | Quality                                                         |
| ----------------------------------------------------------------------- | --------------------------------------------------------------- |
| English, digitally generated: exported PDFs, slides, web pages, reports | Strongest                                                       |
| Scanned pages and photos                                                | Best effort                                                     |
| Handwriting, non-Latin scripts                                          | Not a good fit; use a dedicated OCR system when stakes are high |

## Multi-page PDFs

Holo reads images, not PDFs, so rasterize each page to an image and transcribe them one per request, then stitch the results. One page per request keeps each image at full resolution and is the most reliable pattern.

<CodeGroup>
  ```python Python theme={"system"}
  import base64
  import pymupdf  # pip install pymupdf

  def ocr_page(png_bytes: bytes) -> str:
      data_uri = "data:image/png;base64," + base64.b64encode(png_bytes).decode()
      response = client.chat.completions.create(
          model="holo3-1-35b-a3b",
          messages=[{
              "role": "user",
              "content": [
                  {"type": "image_url", "image_url": {"url": data_uri}},
                  {"type": "text", "text": OCR_PROMPT},
              ],
          }],
          temperature=0.0,
          extra_body={"chat_template_kwargs": {"enable_thinking": False}},
      )
      return response.choices[0].message.content or ""

  with pymupdf.open("document.pdf") as doc:
      pages = [ocr_page(page.get_pixmap(dpi=200).tobytes("png")) for page in doc]

  markdown = "\n\n".join(pages)
  print(markdown)
  ```

  ```typescript TypeScript theme={"system"}
  import { pdf } from "pdf-to-img"; // npm install pdf-to-img

  async function ocrPage(png: Buffer): Promise<string> {
    const dataUri = "data:image/png;base64," + png.toString("base64");
    const response = await client.chat.completions.create({
      model: "holo3-1-35b-a3b",
      messages: [
        {
          role: "user",
          content: [
            { type: "image_url", image_url: { url: dataUri } },
            { type: "text", text: OCR_PROMPT },
          ],
        },
      ],
      temperature: 0.0,
      ...({ chat_template_kwargs: { enable_thinking: false } } as any),
    });
    return response.choices[0].message.content ?? "";
  }

  const pages: string[] = [];
  for await (const page of await pdf("document.pdf", { scale: 2 })) {
    pages.push(await ocrPage(page));
  }

  const markdown = pages.join("\n\n");
  console.log(markdown);
  ```
</CodeGroup>

<Notice kind="note" title="Resolution and throughput">
  Rasterize at 200 to 300 DPI (`scale: 2` to `3`). Lower resolution loses small text. Above that the server downsizes the image, so extra DPI only costs upload bandwidth. Run pages concurrently to speed up long documents, within your [rate limit](/models-api/api-reference#rate-limits).
</Notice>

<Notice kind="gotcha" title="Dense pages can truncate">
  Output is capped at 8,192 tokens per request, and a dense page (large tables, small print) can exceed that. Check `finish_reason` and treat `length` as a truncated transcription. Split dense pages into two images.
</Notice>

## Next steps

<CardGroup cols={3}>
  <Card title="Element localization" icon="crosshairs" href="/models-api/element-localization">
    Get click coordinates from a screenshot.
  </Card>

  <Card title="Build an agent" icon="arrows-rotate" href="/models-api/build-an-agent/core-concepts">
    How to use Holo in your computer-use harness.
  </Card>

  <Card title="API reference" icon="code" href="/models-api/api-reference">
    Endpoint, auth, rate limits, and errors.
  </Card>
</CardGroup>
