Skip to main content
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.

Parse a document

Get the whole document back as structured chunks. Start here. Your first call takes about five minutes.

Extract specific fields

Hand us a JSON schema and get back just those fields, with a citation for every value.

Process documents in bulk

Upload thousands of files, submit one batch, poll one endpoint.
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.
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.

Quickstart

1

Create an API key

Sign up and create a key at 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.
2

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).
3

Read the response

You’ll get back a chunks array, the whole document in reading order:
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).

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.

Office documents (DOCX / PPTX)

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.

The whole document as one string

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. 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. Batches take the same parsing options as sync, set once per batch (except include_content, which is sync-only); see 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).
  • 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 (/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. Every successfully billed response (sync and batch item results alike) includes a usage object telling you exactly what the request cost:
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:

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. For bulk workloads (thousands of documents), don’t loop POST /v1/parse/file. Use the async batch endpoints 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. 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
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 to set up a PHI account.

Data retention

What we keep, lane by lane: We never train on customer data. Need custom retention terms (shorter windows, customer-managed encryption, dedicated regions)? Email 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. Async-batch endpoints (/v1/files, /v1/batches) additionally return machine-readable error codes in the response body. See Batch errors for the full list, response shapes, and fixes.
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.

Next steps

You’ve made your first call. From here:

Extract specific fields

Skip the chunks entirely: define a JSON schema and get back just the fields you care about, each with a citation you can verify.

Go from one document to thousands

The async batch lane: presigned uploads, one submission, one polling loop. Same response schema as sync.
Or open the API Reference in the sidebar for a live playground and the full request/response schema.