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

# Schema extraction

> Fill an arbitrary JSON schema from a document, with a per-field citation for every value.

Tell us which fields you want (`invoice_number`, `total`, `line_items[]`) and get back **just those fields, filled from the document**. Every value arrives with proof: the page it came from, a bounding box you can highlight, and the verbatim source text.

That's the difference from [parsing](/introduction): `POST /v1/parse` gives you the whole document as structured chunks. Schema extraction reads the document *for* you and answers in exactly the shape your application expects.

## Request

`POST /v1/extract/schema` (JSON body with a `url`) or `POST /v1/extract/schema/file` (multipart upload; required for PHI keys). The document can be any supported input: PDF, PPTX, DOCX, or an image (PNG, JPEG, WebP, TIFF, HEIC/HEIF, BMP).

The `schema` is standard JSON Schema, and it is the whole interface: the fields you define are the request, and their descriptions are instructions the extractor follows. A one-line description is the cheapest accuracy lever you have. Say what the field looks like on the page, not just what it means:

```json theme={null}
{
  "invoice_number": { "type": "string", "description": "Invoice ID as printed." },
  "total":          { "type": "number", "description": "Amount due, in dollars." },
  "line_items": {
    "type": "array",
    "items": { "type": "object", "properties": {
      "description": { "type": "string" },
      "amount":      { "type": "number" }
    }}
  }
}
```

**Supported subset:** `string` / `number` / `integer` / `boolean`, `object` (with `properties`), `array` (with `items`), plus `enum` and `description` on leaves. Nesting up to **5 levels**. Wide schemas are capped (a few hundred fields) and return `422` with a "split your schema" message, so keep schemas focused on one document type.

<Tip>
  Use `enum` for fields with a known set of values (status codes, document types, states). It stops casing and phrasing drift: you'll get `"approved"` every time instead of `"Approved"`, `"APPROVED"`, and `"approved ✓"` across documents.
</Tip>

### Request options

The schema is usually all you send. Four more fields tune a run:

| Field              | Type    | Default | What it does                                                                                                                                                                                                                       |
| ------------------ | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `strict`           | boolean | `true`  | Controls what happens to a value whose citation can't be verified. See [Response](#response).                                                                                                                                      |
| `auto_schema`      | boolean | `false` | No schema yet? Omit `schema` and set this to `true`: we design a flat schema from the document, fill it, and return the schema we used as `generated_schema`. Good for exploring a new document type before you commit to a shape. |
| `extract_images`   | boolean | `true`  | Include figures from the parse stage in the extraction context. Set `false` to extract from text and tables only.                                                                                                                  |
| `include_ocr_text` | boolean | `false` | `true` additionally returns [`ocr_text`](#response): the whole document as a single text string, in reading order. The default response is unchanged.                                                                              |

## Response

`values` is your schema, filled in, with `null` where the document genuinely lacks a field. `evidence` is a parallel map keyed by **field path**: each value's source page, box, verified text, and a `confidence` score.

```json theme={null}
{
  "values": {
    "invoice_number": "INV-42",
    "total": 100.0,
    "line_items": [{ "description": "Widget A", "amount": 80.0 }]
  },
  "evidence": {
    "invoice_number":      [{ "page": 1, "bbox": [120, 80, 240, 96], "text": "Invoice #INV-42", "confidence": 0.99 }],
    "total":               [{ "page": 1, "bbox": [410, 540, 470, 556], "text": "Total due $100.00", "confidence": 1.0 }],
    "line_items[0].amount":[{ "page": 1, "bbox": [410, 300, 470, 316], "text": "Widget A ... 80.00", "confidence": 0.98 }]
  },
  "ungrounded_fields": [],
  "ocr_text": "ACME Supply Co.\nInvoice #INV-42\nWidget A ... 80.00\nTotal due $100.00",
  "page_count": 1,
  "usage": { "pages": 1, "credits": 4.0, "credits_per_page": 4.0 }
}
```

Three fields do the heavy lifting:

* **`strict`** (default `true`): a non-null value whose quote can't be verified against the page is **nulled out** and listed in `ungrounded_fields`. This is the safe default: a fabricated value never reaches you. Set `strict: false` to keep such values but still see them flagged.
* **`bbox`**: `[x0, y0, x1, y1]` normalized to **0-1000**, page-relative, top-left origin. Overlay it directly on a page render without fetching page dimensions. It can be `null` when the cited text has no box to point at (converted office formats like DOCX and PPTX don't always carry one). Handle that case before overlaying.
* **`confidence`**: `0` to `1`, scored on the *hardest-to-read character* of the cited text, so one shaky digit lowers the whole score instead of hiding behind an otherwise clear value. It's `null` when a citation doesn't have a score. Route low-confidence values to human review.

<Note>
  Some citations may also carry `needs_review: true` and a `suggested_value`: a hint that an identifier was hard to read and is worth confirming. The value in `values` is never overwritten. These fields are additive; treat their absence as `false` / `null`.
</Note>

On `auto_schema` runs the response also includes `generated_schema`: the schema we designed and filled. Save it and pass it as `schema` on later calls to lock the shape in.

## Example

<CodeGroup>
  ```python python theme={null}
  import json, os, httpx

  API = "https://api.hanji.dev"
  schema = {
      "invoice_number": {"type": "string", "description": "Invoice ID as printed."},
      "total":          {"type": "number", "description": "Amount due, in dollars."},
  }

  with open("invoice.pdf", "rb") as f:
      r = httpx.post(
          f"{API}/v1/extract/schema/file",
          headers={"X-API-KEY": os.environ["HANJI_API_KEY"]},
          files={"file": ("invoice.pdf", f, "application/pdf")},
          data={"schema": json.dumps(schema)},
          timeout=300,
      )
  r.raise_for_status()
  out = r.json()
  print(out["values"])                       # {'invoice_number': 'INV-42', 'total': 100.0}
  print(out["evidence"]["total"][0])         # {'page': 1, 'bbox': [...], 'text': 'Total due $100.00'}
  ```

  ```bash curl theme={null}
  curl -X POST https://api.hanji.dev/v1/extract/schema/file \
    -H "X-API-KEY: $HANJI_API_KEY" \
    -F "file=@invoice.pdf" \
    -F 'schema={"invoice_number":{"type":"string"},"total":{"type":"number"}}'
  ```
</CodeGroup>

## Billing

**4 credits per page** — \$12 per 1,000 pages, all-in. The parse is included, so you're not paying the 1-credit parsing rate on top. Billed responses carry a [`usage`](/introduction#pricing-and-credits) object with the exact `pages`, `credits`, and `credits_per_page` charged. See [Pricing](https://hanji.dev/pricing).

## Next steps

<CardGroup cols={2}>
  <Card title="Parse the whole document" icon="file-lines" href="/introduction#quickstart">
    Need every chunk, table, and figure instead of specific fields? Start with the parse quickstart.
  </Card>

  <Card title="Run schemas at scale" icon="layer-group" href="/guides/batch">
    Processing thousands of documents? Use the async batch lane and keep the same response schema.
  </Card>
</CardGroup>
