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

# Introduction

> Text, tables, and figures in one call, at least 2x faster than other parsers. Fill your own JSON schema with cited values, grounded to the exact spot on the page.

Hanji turns documents into structured JSON. Send a PDF, PPTX, DOCX, or image; get back every run of text, every table as structured cells (plus a markdown rendering), and every figure, in reading order, in one API call.

Everything you get back is grounded. Each chunk carries its page number and bounding box, so you can always point back to the exact spot on the page it came from.

<CardGroup cols={3}>
  <Card title="Parse a document" icon="file-lines" href="#quickstart">
    Get the whole document back as structured chunks. Start here. Your first call takes about five minutes.
  </Card>

  <Card title="Extract specific fields" icon="list-check" href="/guides/schema-extraction">
    Hand us a JSON schema and get back just those fields, with a citation for every value.
  </Card>

  <Card title="Process documents in bulk" icon="layer-group" href="/guides/batch">
    Upload thousands of files, submit one batch, poll one endpoint.
  </Card>
</CardGroup>

All of it runs on the same engine and shares one response schema. A chunk from a sync call looks identical to a chunk from a batch item, so learn the response shape once and it works everywhere.

<Note>
  **Path rename (compatibility).** Sync parse is `POST /v1/parse` and `POST /v1/parse/file`. The previous paths `POST /v1/extract` and `POST /v1/extract/file` still work and return the same responses; prefer `/v1/parse` for new integrations.
</Note>

## Quickstart

<Steps>
  <Step title="Create an API key">
    Sign up and create a key at [hanji.dev/dashboard](https://hanji.dev/dashboard). The Free plan includes **1,000 credits** (one parsed page = 1 credit), enough to parse a few hundred real documents. The key is shown once on creation, so store it somewhere safe.

    ```bash theme={null}
    export HANJI_API_KEY="your-key-here"
    ```
  </Step>

  <Step title="Parse your first document">
    Pick the path that matches where your document lives. Pass a `url` if it's already reachable over HTTP (an S3 presigned URL, a public doc, a CDN). Upload the file directly if you have the bytes in hand (agent output, local file, webhook payload).

    <Tabs>
      <Tab title="By URL">
        <CodeGroup>
          ```bash curl theme={null}
          curl -X POST https://api.hanji.dev/v1/parse \
            -H "X-API-KEY: $HANJI_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "url": "https://arxiv.org/pdf/1706.03762.pdf"
            }'
          ```

          ```python python theme={null}
          import os, requests

          r = requests.post(
              "https://api.hanji.dev/v1/parse",
              headers={"X-API-KEY": os.environ["HANJI_API_KEY"]},
              json={"url": "https://arxiv.org/pdf/1706.03762.pdf"},
              timeout=120,
          )
          r.raise_for_status()
          doc = r.json()
          print(len(doc["chunks"]), "chunks")
          ```

          ```javascript javascript theme={null}
          const r = await fetch("https://api.hanji.dev/v1/parse", {
            method: "POST",
            headers: {
              "X-API-KEY": process.env.HANJI_API_KEY,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ url: "https://arxiv.org/pdf/1706.03762.pdf" }),
          });
          const doc = await r.json();
          console.log(doc.chunks.length, "chunks");
          ```
        </CodeGroup>
      </Tab>

      <Tab title="By upload">
        <CodeGroup>
          ```bash curl theme={null}
          curl -X POST https://api.hanji.dev/v1/parse/file \
            -H "X-API-KEY: $HANJI_API_KEY" \
            -F "file=@paper.pdf"
          ```

          ```python python theme={null}
          import os, requests

          with open("paper.pdf", "rb") as f:
              r = requests.post(
                  "https://api.hanji.dev/v1/parse/file",
                  headers={"X-API-KEY": os.environ["HANJI_API_KEY"]},
                  files={"file": ("paper.pdf", f, "application/pdf")},
                  timeout=120,
              )
          r.raise_for_status()
          doc = r.json()
          print(len(doc["chunks"]), "chunks")
          ```

          ```javascript javascript theme={null}
          import { readFileSync } from "node:fs";

          const form = new FormData();
          form.set("file", new Blob([readFileSync("paper.pdf")]), "paper.pdf");

          const r = await fetch("https://api.hanji.dev/v1/parse/file", {
            method: "POST",
            headers: { "X-API-KEY": process.env.HANJI_API_KEY },
            body: form,
          });
          const doc = await r.json();
          console.log(doc.chunks.length, "chunks");
          ```
        </CodeGroup>

        <Note>
          The filename doesn't matter; we look at the file itself. A `.docx` name on PDF bytes is treated as a PDF. The form accepts the same `extract_text`, `extract_images`, `ocr`, `table_output_format`, `chunking`, and `include_content` fields as the JSON route; send them as individual form fields.
        </Note>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Read the response">
    You'll get back a `chunks` array, the whole document in reading order:

    ```json theme={null}
    {
      "chunks": [
        {
          "page_content": "Attention Is All You Need",
          "page_no": 1,
          "bbox": [176.6, 88.7, 438.3, 107.2],
          "chunk_type": "text"
        },
        {
          "page_content": "| Model | BLEU |\n|---|---|\n| Transformer | 28.4 |",
          "page_no": 3,
          "bbox": [110.0, 200.4, 500.0, 320.1],
          "chunk_type": "table",
          "n_rows": 2,
          "n_cols": 2,
          "cells": [
            { "text": "Model", "row": 0, "col": 0 },
            { "text": "BLEU", "row": 0, "col": 1 },
            { "text": "Transformer", "row": 1, "col": 0 },
            { "text": "28.4", "row": 1, "col": 1 }
          ]
        },
        {
          "page_content": "",
          "page_no": 4,
          "bbox": [108.0, 281.4, 504.0, 531.4],
          "chunk_type": "image",
          "image_url": "https://...",
          "image_mime": "image/webp",
          "image_width": 1188,
          "image_height": 750
        }
      ]
    }
    ```

    That's it. You've parsed your first document. The rest of this page covers the response in detail, the request options, and the operational stuff (billing, limits, errors).
  </Step>
</Steps>

## Understanding the response

Every chunk is one of three types:

* **`text`**: a short run of text in one style. Not a full paragraph; a paragraph usually splits into several text chunks.
* **`table`**: a table. `cells` is the structured representation (0-based `row`/`col`, with `row_span`/`col_span` for merged cells), and `page_content` carries a markdown rendering so plain-text consumers still get readable output — request `table_output_format: "html"` to get that same table as HTML instead. By default, cell `bbox` values on scanned tables share the whole table's box; request `table_output_format: "cell_grid"` to get a true box per cell.
* **`image`**: a figure extracted from the page, delivered as a URL or inline base64.

And every chunk carries:

* **`bbox`**: `[x0, y0, x1, y1]` in PDF points, so you can highlight the source region on the page.
* **`page_no`**: the 1-based page the chunk came from.
* **`confidence`**: `0` to `1` for OCR'd content, scored on the hardest-to-read character of the chunk, so one shaky digit lowers the whole score. `null` for text read from the document's own text layer, where recognition isn't a factor. Route low-confidence chunks to review.

<h3 id="pdf-rendition">
  Office documents (DOCX / PPTX)
</h3>

A `.docx` or `.pptx` is converted to PDF before parse, and every `bbox` is in **that** PDF's point space. The original Word/PowerPoint file does not paginate the same way, so drawing those boxes on your own render of the `.docx` will miss. For these inputs the response also includes `pdf_rendition_url`: an expiring link to the converted PDF. Download it onto your own storage and overlay `bbox` + `page_no` exactly as you would for a native PDF. The field is omitted (not `null`) for PDFs and images — you already have the pages.

Type-specific fields:

* `cells` / `n_rows` / `n_cols`: populated on table chunks. Each cell is `{ text, row, col, row_span, col_span, bbox, confidence }`.
* `image_url` / `image_mime`: populated on image chunks. Some accounts receive the image inline as `image_b64` instead.

<h3 id="content">
  The whole document as one string
</h3>

Set `include_content: true` and the response carries one extra field alongside `chunks`: `content` — the entire parsed document as a single string, concatenated in reading order. Text flows as paragraphs, tables render as markdown tables in place, and figures are omitted. It is the document's own text end to end, not reformatted with markdown headings or page markers. Use it when you want to hand the whole document to an LLM in one shot rather than iterate over chunks. The default response is unchanged.

## Request options

For the URL route (`POST /v1/parse`), `url` is required and everything else is optional. For the upload route (`POST /v1/parse/file`), `file` is required and the remaining fields arrive as individual form fields instead of JSON.

| Field                 | Type                                  | Default      | Description                                                                                                                                                                                                                                                                                                |
| --------------------- | ------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`                 | string                                | -            | URL route only. HTTP(S) URL to a supported document or image. The type is detected automatically.                                                                                                                                                                                                          |
| `file`                | binary                                | -            | Upload route only. Multipart file field. The type is detected from the file itself; the filename doesn't matter.                                                                                                                                                                                           |
| `extract_text`        | boolean                               | `true`       | Set `false` to skip text spans. Table chunks (and figures, when `extract_images` is true) are still returned.                                                                                                                                                                                              |
| `extract_images`      | boolean                               | `true`       | Set `false` to skip figure extraction; text and table chunks are still returned.                                                                                                                                                                                                                           |
| `ocr`                 | `"auto" \| "never" \| "force"`        | `"auto"`     | *Deprecated; accepted but ignored.* This field no longer changes behavior.                                                                                                                                                                                                                                 |
| `table_output_format` | `"markdown" \| "html" \| "cell_grid"` | `"markdown"` | `"html"` puts an HTML table in `page_content`; `"cell_grid"` returns true per-cell boxes. Echoed on each table chunk.                                                                                                                                                                                      |
| `chunking`            | `"none" \| "semantic"`                | `"none"`     | `"semantic"` additionally returns [`segments`](#chunking), the document grouped into pieces sized for RAG. The default response is unchanged.                                                                                                                                                              |
| `chunk_size`          | integer                               | `1000`       | Target segment size in characters. Segments land within ±25% of the target (default 750 to 1250). Sizing is approximate: an oversized element or a final remainder can fall outside the band. Only meaningful when `chunking` is enabled; validated (200 to 8000) only in that case and ignored otherwise. |
| `include_content`     | boolean                               | `false`      | `true` additionally returns [`content`](#content): the entire parsed document as a single text string, concatenated in reading order. The default response is unchanged.                                                                                                                                   |

Schema extraction (`POST /v1/extract/schema`) has its own options: `schema`, `strict`, `auto_schema`, and `include_ocr_text` (the `include_content` of that route). See the [Schema extraction guide](/guides/schema-extraction#request-options). Batches take the same parsing options as sync, set once per batch (except `include_content`, which is sync-only); see [Batch options](/guides/batch#batch-options).

**Supported input formats:** PDF, PPTX, DOCX, and images (PNG, JPEG, WebP, TIFF, HEIC/HEIF, and BMP). An image is treated as a one-page document, and the response looks the same as for a PDF. Animated GIF and animated WebP are rejected with `400`.

## Chunking

Set `chunking: "semantic"` and the response carries two extra fields alongside the unchanged `chunks` array: `segments` and `page_dimensions`.

Each segment is a group of chunks sized toward `chunk_size` characters, ready to embed, split at natural boundaries (headings, figures with their captions, page breaks). `content` is markdown. Tables come as markdown tables; images are not included. A table too large for one segment is split at row boundaries, and every part remains a valid table (`table_part` records the rows each part covers).

```json theme={null}
{
  "chunks": [ /* the unchanged flat list; full payloads live here */ ],
  "page_dimensions": [ { "page_no": 1, "width": 612.0, "height": 792.0 } ],
  "segments": [
    {
      "content": "3.1 Encoder and Decoder Stacks\n\nThe encoder is composed of...",
      "char_count": 987,
      "pages": [3],
      "chunks": [
        { "source_index": 41, "chunk_type": "text", "page_no": 3, "bbox": [64.3, 585.2, 528.0, 601.9] },
        { "source_index": 42, "chunk_type": "text", "page_no": 3, "bbox": [64.3, 610.0, 528.0, 719.5] }
      ]
    }
  ]
}
```

* `content`: the segment's text, formatted as markdown. Use this field to pass the text to an embedding model.
* `char_count`: the number of characters in `content`.
* `pages`: the 1-based page numbers the segment spans.
* `chunks`: the elements that make up the segment. Each includes its `chunk_type`, `page_no`, and `bbox`.
* `source_index`: the element's index in the response's `chunks` array, where the full element lives.
* `page_dimensions`: the width and height of each page, in the same units as `bbox`.

## Pricing and credits

Billing is in **credits**, at **\$3 per 1,000 credits** (\$0.003 per credit). Each page you process costs the operation's credit rate:

* **Parsing** (`/v1/parse`, `/v1/parse/file`, and the batch lane): **1 credit per page** — \$3 per 1,000 pages.
* **[Schema extraction](/guides/schema-extraction)** (`/v1/extract/schema`): **4 credits per page** — \$12 per 1,000 pages, all-in. The 4 credits **include the parse**; it is not billed on top of the parsing rate.

There are no configuration surcharges: chunking, images, and cell-level tables are included in those rates. Your remaining balance is always visible in the [dashboard](https://hanji.dev/dashboard).

Every successfully billed response (sync and batch item results alike) includes a `usage` object telling you exactly what the request cost:

```json theme={null}
{ "usage": { "pages": 12, "credits": 12.0, "credits_per_page": 1.0 } }
```

`pages` is the source document's page count, `credits` is the total charged, and `credits_per_page` is the rate applied. On requests that aren't billed, the field is absent, not `null`.

What counts as a page depends on the input:

| Input                                    | Page counting                                          |
| ---------------------------------------- | ------------------------------------------------------ |
| PDF                                      | One page per PDF page                                  |
| PPTX                                     | One page per slide                                     |
| DOCX                                     | Paginated on render; typically 250-400 words per page  |
| Images (PNG, JPEG, WebP, HEIC/HEIF, BMP) | One page per image                                     |
| TIFF                                     | One page per frame; a 10-frame fax TIFF bills 10 pages |

## Limits and large documents

Two server-side limits apply to every document:

* **Max 2,000 pages per document.** Larger documents fail with `413`.
* **Max 150 MB per document.** Larger downloads fail with `413`.

For documents over those limits, split client-side and concatenate the `chunks` arrays; the `page_no` field lets you offset page numbers across splits. Need support for individual documents beyond 2,000 pages? Email [hello@hanji.dev](mailto:hello@hanji.dev).

For bulk workloads (thousands of documents), don't loop `POST /v1/parse/file`. Use the **[async batch endpoints](/guides/batch)** instead: you upload each file once to a presigned URL, submit the whole set as one batch, and poll for completion. Same response schema, no per-doc HTTP round-trip overhead.

## Authentication

Every request needs an `X-API-KEY` header. Keys are created and revoked from the [dashboard](https://hanji.dev/dashboard). Each key:

* is bound to one customer account
* carries a quota expressed in credits (default **1,000 credits** on the Free plan)
* goes down with each request by the document's page count times the operation's [credit rate](#pricing-and-credits)

You can rotate a key at any time; the new one is returned once on creation and never shown again.

If your account is provisioned for PHI, your keys behave a little differently: extracted images stay inline in the response (never S3), logging is allowlisted, and filenames are not retained. URL and multipart parse both work (`/v1/parse`, `/v1/parse/file`). Email [hello@hanji.dev](mailto:hello@hanji.dev) to set up a PHI account.

## Data retention

What we keep, lane by lane:

| Data                                                | Retention                                                                                                                                             |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Sync documents (`/v1/parse*`, alias `/v1/extract*`) | Never stored. Processed in memory; gone when the response returns.                                                                                    |
| Office PDF rendition (`pdf_rendition_url`)          | DOCX/PPTX only. An expiring download link to the PDF we paginated; copy it to your storage. The object is deleted automatically after 3 days.         |
| Batch uploads (`POST /v1/files`)                    | Deleted automatically 3 days after upload.                                                                                                            |
| Batch results                                       | Deleted automatically 3 days after item completion.                                                                                                   |
| URL-sourced batch inputs                            | Never stored. Fetched into memory at processing time.                                                                                                 |
| Extracted images (`extract_images: true`)           | Only created when you request them. Stored so your `image_url` links keep working; deleted on request ([hello@hanji.dev](mailto:hello@hanji.dev)).    |
| Request log                                         | Metadata only: ids, page counts, timings, cost. Never documents, results, filenames, or response bodies. Rows kept 90 days.                           |
| Page-quality diagnostics                            | A page that fails an automated quality check may be kept as a rendered image for internal review and is deleted within 7 days. Never on PHI accounts. |

We never train on customer data. Need custom retention terms (shorter windows, customer-managed encryption, dedicated regions)? Email [hello@hanji.dev](mailto:hello@hanji.dev).

## Errors

Errors come back as standard HTTP status codes with a JSON body. The short version: `4xx` means fix the request, `5xx` means retry.

| Status | Meaning                                                   | What to do                                                                                                                                                                                      |
| ------ | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Unsupported input or extraction failed                    | Check the `url` points to a supported format (PDF, PPTX, DOCX, or a supported image: PNG, JPEG, WebP, TIFF, HEIC/HEIF, BMP; animated images are rejected) and that the document isn't corrupted |
| `401`  | Missing or invalid `X-API-KEY`                            | Check the header is set; re-create the key if revoked                                                                                                                                           |
| `402`  | Quota exceeded                                            | Top up from the dashboard or wait for plan refresh                                                                                                                                              |
| `404`  | File or batch not found (async batch only)                | Verify the `file_id` / `batch_id` is correct and not expired (3-day TTL)                                                                                                                        |
| `409`  | File not uploaded, or result not ready (async batch only) | See [Batch errors](/guides/batch#errors)                                                                                                                                                        |
| `413`  | Page limit or size limit exceeded                         | Split the document client-side                                                                                                                                                                  |
| `422`  | Request body invalid                                      | The error body says which field is wrong (`detail[*].loc`) and why (`detail[*].msg`); usually a missing `url`                                                                                   |
| `429`  | Rate limit exceeded                                       | Back off and retry                                                                                                                                                                              |
| `500`  | Server error                                              | Retry with exponential backoff; contact support if persistent                                                                                                                                   |
| `503`  | Temporarily unavailable                                   | Retry with backoff; failed requests are not billed                                                                                                                                              |

Async-batch endpoints (`/v1/files`, `/v1/batches`) additionally return machine-readable `error` codes in the response body. See **[Batch errors](/guides/batch#errors)** for the full list, response shapes, and fixes.

<Tip>
  Pass your own `X-Request-Id` header if you want to correlate logs with us. It shows up on our side too, which makes support conversations much faster.
</Tip>

## Next steps

You've made your first call. From here:

<CardGroup cols={2}>
  <Card title="Extract specific fields" icon="list-check" href="/guides/schema-extraction">
    Skip the chunks entirely: define a JSON schema and get back just the fields you care about, each with a citation you can verify.
  </Card>

  <Card title="Go from one document to thousands" icon="layer-group" href="/guides/batch">
    The async batch lane: presigned uploads, one submission, one polling loop. Same response schema as sync.
  </Card>
</CardGroup>

Or open the **API Reference** in the sidebar for a live playground and the full request/response schema.
