# Document Extraction

> Parses documents into structured Markdown, extracts fields with JSON schemas, classifies pages, and splits mixed document batches using LandingAI's Agentic Document Extraction (ADE) REST APIs, with official Python and TypeScript libraries. Builds document pipelines: batch and async processing for large files, classify-then-extract routing, RAG chunking and embeddings, multi-page table stitching, bounding box visualization, cropping, word-level highlighting, and grounding extracted fields to their source citations. Use when processing PDFs, images, scans, Office documents (Word, PowerPoint), invoices, forms, or bank statements, when migrating between ADE API versions, or when the user mentions ADE, parsing, extraction, classification, document splitting, table of contents generation, grounding, bounding boxes, blocks, chunks, ranges, citations, or word confidence scores.

- Skill: `landing-ai/document-extraction` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add landing-ai/document-extraction`
- Raw SKILL.md: https://api.skillmd.com/api/skills/landing-ai/document-extraction/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: landing-ai (https://skillmd.com/u/landing-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/landing-ai/document-extraction

---


# Document Extraction (ADE)

## Overview

LandingAI's Agentic Document Extraction (ADE) is a document processing service that parses, extracts, and classifies documents without templates or training. The REST APIs are the primary interface. Official libraries wrap them for Python (`landingai-ade` on PyPI) and TypeScript (`landingai-ade` on npm).

ADE has two API generations. The **v2 APIs** (powered by DPT-3) are the current generation for parsing and extraction. Several capabilities exist only as **v1 APIs** and remain fully supported.

| API | Version | Endpoint | Guide |
|-----|---------|----------|-------|
| Parse | v2 | `POST https://api.ade.landing.ai/v2/parse` | https://docs.landing.ai/dpt3/parse |
| Parse Jobs | v2 | `POST/GET https://api.ade.landing.ai/v2/parse/jobs` | https://docs.landing.ai/dpt3/parse-async |
| Extract | v2 | `POST https://api.ade.landing.ai/v2/extract` | https://docs.landing.ai/dpt3/extract |
| Extract Jobs | v2 | `POST/GET https://api.ade.landing.ai/v2/extract/jobs` | https://docs.landing.ai/dpt3/extract-async |
| Ground | v2 | `POST https://api.ade.landing.ai/v2/ground` | https://docs.landing.ai/dpt3/ground |
| Classify | v1 | `POST https://api.va.landing.ai/v1/ade/classify` | https://docs.landing.ai/ade/ade-classify |
| Section | v1 | `POST https://api.va.landing.ai/v1/ade/section` | https://docs.landing.ai/ade/ade-section |
| Build Extract Schema | v1 | `POST https://api.va.landing.ai/v1/ade/extract/build-schema` | https://docs.landing.ai/ade/ade-extract-schema-api |
| Split | v1 | `POST https://api.va.landing.ai/v1/ade/split` | https://docs.landing.ai/ade/ade-split |
| Parse (superseded) | v1 | `POST https://api.va.landing.ai/v1/ade/parse` | https://docs.landing.ai/ade/parse |
| Extract (superseded) | v1 | `POST https://api.va.landing.ai/v1/ade/extract` | https://docs.landing.ai/ade/ade-extract |

Every linked docs page can be fetched as raw Markdown by appending `.md` to its URL (for example, `https://docs.landing.ai/dpt3/parse.md`). A page index lives at https://docs.landing.ai/llms.txt. Full request and response contracts are in the API reference (linked per endpoint below).

## Which API Version? {#which-api-version}

**Use the v2 APIs by default.** Route to v1 only when one of these applies:

- The user has existing code calling the `/v1/ade/*` endpoints (or v1 library methods `client.parse()` / `client.extract()`) and has not asked to migrate.
- The document is a spreadsheet (XLSX, CSV) or a legacy binary Office file (DOC, PPT); only v1 Parse accepts those. v2 Parse accepts PDFs, images, and modern Office documents (DOCX, PPTX, ODT, RTF).
- The file is password-protected (v2 rejects it with HTTP 422; v1 accepts a `password` parameter).
- The pipeline needs custom figure prompts or v1-style page splits (`split=page`).
- The pipeline feeds Parse output into the v1 Section or v1 Split APIs, which require the v1 Parse response shape.

To move an existing v1 pipeline to v2, follow https://docs.landing.ai/dpt3/migration-guide.

**Do not mix versions within one pipeline**, except as this matrix allows:

| Markdown produced by | v2 Extract | v1 Extract | v1 Section | v1 Split |
|---|---|---|---|---|
| Parse v2 | Yes (preferred; reads the embedded `doc_id`) | **No** | **No** | No (use v1 Parse for Split pipelines) |
| Parse v1 | Yes (works, but no `doc_id` link) | Yes | Yes | Yes |

The v1 Classify API takes the raw document, not Parse output, so it composes with either version.

## API Drift: Your Prior Knowledge May Be Stale

If you have seen ADE code before, it was probably v1. These v1 idioms cause silent wrong-output bugs in v2 code:

| Stale (v1) pattern | Current (v2) |
|---|---|
| `chunks` list in the response | `structure` tree of pages and blocks; slice `markdown` with each block's `grounding.range` |
| 0-indexed page numbers | Pages are **1-indexed** in v2: `grounding.page`, `metadata.failed_pages`, `options.pages` |
| Box keys `left/top/right/bottom` | Renamed `xmin/ymin/xmax/ymax` (still normalized 0 to 1) |
| `model=dpt-2-latest` | `model=dpt-3-pro-latest` (pin a dated snapshot such as `dpt-3-pro-20260710` in production) |
| `confidence`, `low_confidence_spans` | Removed in v2. The DPT-3 Verity model (preview; formerly DPT-3 Fast) returns a different signal: per-word `confidence` on `atomic_grounding` entries only, never on block, table, or page groundings |

For the full v1-to-v2 request and response mapping, see https://docs.landing.ai/dpt3/migration-guide.

## Setup

### API Key

All endpoints authenticate with the same header: `Authorization: Bearer YOUR_API_KEY`. Get a key at https://va.landing.ai/settings/api-key and set it as the `VISION_AGENT_API_KEY` environment variable (both libraries read it automatically). Before asking the user for a key, check for an existing `.env` file in the working directory and in this skill's own directory (`skills/document-extraction/.env`); a `.env-sample` template sits next to this file. Keys are region-specific; for EU endpoints and data residency see https://docs.landing.ai/dpt3/eu.

### Libraries (optional)

- **Python:** `pip install landingai-ade` (v1.17.0 or later covers everything in this skill). Guide: https://docs.landing.ai/dpt3/ade-python
- **TypeScript:** `npm install landingai-ade` (v2.12.0 or later covers everything in this skill). Guide: https://docs.landing.ai/dpt3/ade-typescript

When writing scripts, use the user's language and environment. Never install packages globally; use the project's virtualenv or package.json.

## Core Flow: Parse, Extract, Then Ground (v2)

Parse converts a document into Markdown plus structure. Extract pulls schema-defined fields from that Markdown. **Run both as jobs on the `standard` service tier**: create the job, then call `wait()` (or poll `GET .../jobs/{job_id}`) for the finished job. Standard jobs cost half the credits of `priority`; see Processing Modes below for when to leave this default. Keep the trailing `<!-- doc_id=... -->` comment when saving Markdown; v2 Extract reads it to link the extraction back to its parse job.

**Step 1: Parse.** Jobs accept PDFs, images, and Office documents (DOCX, PPTX, ODT, RTF), up to 1 GiB for PDFs and 50 MiB for images; for the current page limits, see https://docs.landing.ai/dpt3/rate-limits. Office files are converted to PDF before parsing; the conversion can change layout and page count, and page-based limits and credits apply to the converted PDF's page count (https://docs.landing.ai/dpt3/file-types). Models: `dpt-3-pro-latest` (default, highest quality) or `dpt-3-verity` (preview: lower latency and credits, for digitally created text documents only; renamed from `dpt-3-fast`, whose values still work). DPT-3 Verity does not read scans, handwriting, or non-Latin scripts, outputs plain Markdown without heading or bold formatting, and adds per-word confidence scores. Comparison and snapshot values: https://docs.landing.ai/dpt3/parse-models.

```bash
mkdir -p output
curl -X POST 'https://api.ade.landing.ai/v2/parse/jobs' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'document=@document.pdf' \
  -F 'model=dpt-3-pro-latest' \
  -F 'service_tier=standard'

# The create response is {"job_id": "...", "status": "pending", ...}.
# Repeat this request with that job_id until status is completed or failed.
# A completed job carries the parse response under result; save result.markdown
# from this file as output/parse-output.md for Step 2 (the library examples do this).
curl 'https://api.ade.landing.ai/v2/parse/jobs/JOB_ID' \
  -H 'Authorization: Bearer YOUR_API_KEY' -o output/parse-response.json
```

```python
from pathlib import Path
from landingai_ade import LandingAIADE

client = LandingAIADE()
Path("output").mkdir(exist_ok=True)

job = client.v2.parse_jobs.create(
    document=Path("document.pdf"),
    model="dpt-3-pro-latest",
    service_tier="standard",
)
# wait() polls until the job finishes. raise_on_failure turns a failed job into
# JobFailedError instead of returning a job whose result is None.
done = client.v2.parse_jobs.wait(job.job_id, timeout=3600, raise_on_failure=True)
Path("output/parse-response.json").write_text(done.result.model_dump_json(indent=2), encoding="utf-8")
Path("output/parse-output.md").write_text(done.result.markdown, encoding="utf-8")
```

```typescript
import fs from "fs";
import LandingAIADE from "landingai-ade";

const client = new LandingAIADE();
fs.mkdirSync("output", { recursive: true });

const job = await client.v2.parseJobs.create({
  document: fs.createReadStream("document.pdf"),
  model: "dpt-3-pro-latest",
  service_tier: "standard",
});
// wait() polls until the job finishes. raiseOnFailure turns a failed job into
// JobFailedError instead of returning a job whose result is null.
const done = await client.v2.parseJobs.wait(job.job_id, { timeout: 3_600_000, raiseOnFailure: true });
const parsed = done.result as LandingAIADE.V2ParseResponse;
if (!parsed.markdown) {
  throw new Error(`Job ${job.job_id} returned no Markdown (status: ${done.status}).`);
}
fs.writeFileSync("output/parse-response.json", JSON.stringify(parsed, null, 2));
fs.writeFileSync("output/parse-output.md", parsed.markdown);
```

Three things to get right with jobs:

- **The output is nested under `result`.** A finished job is `{job_id, status, result, ...}`. When `status` is `completed`, the parse response (`markdown`, `structure`, `metadata`) is `result`, so read `done.result.markdown`, not `done.markdown`. When `status` is `failed`, `result` is null and `error` carries a `code` and `message`; the library `wait()` calls above turn that into `JobFailedError`.
- **`wait()` has a 10-minute default timeout** (`timeout=600` seconds in Python, `timeout: 600000` milliseconds in TypeScript). When it expires, `wait()` raises `JobWaitTimeoutError` but the job keeps running server-side. Pass a longer `timeout` for large documents on `standard`, as above, or catch the error and resume with `parse_jobs.get(job_id)` / `parseJobs.get(jobId)`.
- **Save the full response, not only the Markdown.** Job `create` methods do not accept `save_to` / `saveTo`; write `result` to disk yourself as shown, or pass `output_save_url` (see Processing Modes below). The saved JSON carries `structure` and grounding, which every cropping, table, and RAG workflow below needs.

Useful `options` (multipart field with a JSON value): `{"pages": [1, 3]}` (1-indexed page selection; any page beyond the document's last page rejects the whole request with HTTP 422, or fails the job after it starts), `{"blocks": {"table": {"format": "markdown"}}}` (pipe-syntax tables instead of HTML). Full contract: [Parse API reference](https://docs.landing.ai/api-reference/parse/ade-parse), https://docs.landing.ai/dpt3/parse-input.

**Step 2: Extract.** The schema is a JSON Schema object; descriptions guide the extraction, so treat them as prompts. The optional `model` field pins an extraction model snapshot (`extract-latest` is the default; pin a dated snapshot in production, because a new default snapshot can change extraction results).

```bash
curl -X POST 'https://api.ade.landing.ai/v2/extract/jobs' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'markdown=@output/parse-output.md' \
  -F 'schema={"type":"object","properties":{"invoice_number":{"type":"string","description":"Invoice number"},"total_amount":{"type":"number","description":"Total amount in USD"}}}' \
  -F 'service_tier=standard'

# Repeat with the returned job_id until status is completed or failed.
curl 'https://api.ade.landing.ai/v2/extract/jobs/JOB_ID' \
  -H 'Authorization: Bearer YOUR_API_KEY' -o output/extract-response.json
```

```python
from pathlib import Path
from landingai_ade import LandingAIADE

client = LandingAIADE()

job = client.v2.extract_jobs.create(
    markdown=Path("output/parse-output.md").read_text(encoding="utf-8"),
    schema={
        "type": "object",
        "properties": {
            "invoice_number": {"type": "string", "description": "Invoice number"},
            "total_amount": {"type": "number", "description": "Total amount in USD"},
        },
    },
    service_tier="standard",
)
done = client.v2.extract_jobs.wait(job.job_id, timeout=3600, raise_on_failure=True)
Path("output/extract-response.json").write_text(done.result.model_dump_json(indent=2), encoding="utf-8")
print(done.result.extraction)
```

```typescript
import fs from "fs";
import LandingAIADE from "landingai-ade";

const client = new LandingAIADE();

const job = await client.v2.extractJobs.create({
  markdown: fs.readFileSync("output/parse-output.md", "utf8"),
  schema: {
    type: "object",
    properties: {
      invoice_number: { type: "string", description: "Invoice number" },
      total_amount: { type: "number", description: "Total amount in USD" },
    },
  },
  service_tier: "standard",
});
const done = await client.v2.extractJobs.wait(job.job_id, { timeout: 3_600_000, raiseOnFailure: true });
const extracted = done.result as LandingAIADE.V2ExtractResult;
fs.writeFileSync("output/extract-response.json", JSON.stringify(extracted, null, 2));
console.log(extracted.extraction);
```

The libraries also accept a Pydantic class (Python) or Zod schema (TypeScript) directly on `schema`. Full contract: [Extract API reference](https://docs.landing.ai/api-reference/extract/ade-extract), https://docs.landing.ai/dpt3/extract-input.

**Step 3: Ground (optional).** Ground maps each extracted field back to the parse blocks it was quoted from, returning page numbers and bounding boxes, so you don't have to join ranges by hand. Pass Step 1's `structure` and Step 2's `extraction_metadata`; it runs synchronously, needs no job, and is always free. Each multipart field carries one JSON-serialized object, so write the two out to their own files first: on a curl-saved job response both sit under `.result`, while the library examples above already wrote them unwrapped.

```bash
curl -X POST 'https://api.ade.landing.ai/v2/ground' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'extraction_metadata=<output/extraction-metadata.json' \
  -F 'structure=<output/structure.json' \
  -o output/ground-response.json
```

```python
import json
from pathlib import Path
from landingai_ade import LandingAIADE

client = LandingAIADE()
parse_result = json.loads(Path("output/parse-response.json").read_text())
extract_result = json.loads(Path("output/extract-response.json").read_text())

# A mismatched pair is not an error: Ground would return 200 with wrong boxes.
assert extract_result["metadata"]["doc_id"] == parse_result["metadata"]["job_id"]

ground_response = client.v2.ground(
    extraction_metadata=extract_result["extraction_metadata"],
    structure=parse_result["structure"],
)
print(ground_response.grounding)
```

```typescript
import fs from "fs";
import LandingAIADE from "landingai-ade";

const client = new LandingAIADE();
const parseResult = JSON.parse(fs.readFileSync("output/parse-response.json", "utf8"));
const extractResult = JSON.parse(fs.readFileSync("output/extract-response.json", "utf8"));

// A mismatched pair is not an error: Ground would return 200 with wrong boxes.
if (extractResult.metadata.doc_id !== parseResult.metadata.job_id) {
  throw new Error("The extraction and the structure came from different parses.");
}

const groundResponse = await client.v2.ground({
  extraction_metadata: extractResult.extraction_metadata,
  structure: parseResult.structure,
});
console.log(groundResponse.grounding);
```

**Pair each extraction with its own parse.** Ranges shift between parses, even of the same file, so an extraction grounds only against the `structure` it was extracted from. A mismatched pair is not an error: Ground returns HTTP 200 with plausible boxes that land on the wrong content, and nothing in the response marks it. Check the pairing in code before rendering anything, by comparing the Extract response's `metadata.doc_id` with the Parse response's `metadata.job_id`. Re-parsing a document means re-running its extraction too.

Organizations with Zero Data Retention enabled get HTTP 501 from Ground instead of a result; see "Reading v2 Responses" below for the manual fallback. Full contract: [Ground API reference](https://docs.landing.ai/api-reference/ground/ade-ground), https://docs.landing.ai/dpt3/ground.

## Reading v2 Responses

**Parse** returns three top-level fields (https://docs.landing.ai/dpt3/parse-response); on a finished job they sit under `result`:

- `markdown`: the whole document in reading order. Every `range` in the response indexes into this string using Unicode code point offsets.
- `structure`: a `document` node whose children are pages; each page's children are blocks (`text`, `table`, `table_cell`, `figure`, `marginalia`, `attestation`, `logo`, `card`, `scan_code`). Tables nest their cells. Block ids (`text-0`, `table_cell-3`) are unique per response but not stable across re-parses.
- `metadata`: `job_id`, `model_version`, `page_count`, `failed_pages` (1-indexed), `duration_ms`, `billing`.

Every node carries an inline `grounding` object: `page` (1-indexed), `range` (`{start, end}`, end exclusive), and `box` (`xmin/ymin/xmax/ymax`, each normalized 0 to 1). To get a block's text, slice `markdown[range.start:range.end]`. To get pixels, multiply box values by the rendered page dimensions. Leaf blocks also carry `atomic_grounding` for fine-grained highlighting; its granularity depends on the model. DPT-3 Pro emits one entry per visual line (table cells have none). DPT-3 Verity emits one entry per word, table cells included, and each word entry carries a `confidence` score (0 to 1) for how certain the model is it transcribed the word correctly. Confidence appears only on word entries; block, table, and page groundings never carry one, so to score a block, take the minimum across its word entries. Use low scores to route transcriptions to review, or re-parse the document with DPT-3 Pro. DPT-3 Pro returns no confidence scores.

Markdown format details (page breaks, `<figure>` elements, attestation labels, the trailing `doc_id` comment): https://docs.landing.ai/dpt3/parse-response.

**Extract** returns (https://docs.landing.ai/dpt3/extract-response):

- `extraction`: values matching the schema. Fields the model cannot find come back as `null` (arrays as `[]`).
- `extraction_metadata`: mirrors `extraction` with each leaf replaced by `{"value": ..., "ranges": [...]}`. Each range indexes into the input Markdown; a synthesized value has `null` ranges. For a field's bounding box, call Ground (Core Flow Step 3 above) with this `extraction_metadata` and the parse's `structure`; it does the range-to-block join server-side. Compute it by hand only as the documented fallback for Zero Data Retention organizations, where Ground returns HTTP 501: find the parse blocks whose `grounding.range` overlaps the field's range, then use those blocks' `grounding.box`.
- `metadata.doc_id`: the originating parse job, when the input Markdown carried the `doc_id` comment.

**Ground** returns `grounding`, a tree mirroring the `extraction_metadata` you sent: objects and arrays keep their shape, and each `{value, ranges}` leaf becomes the list of blocks its ranges overlap. Each entry carries `block_id`, `type`, `parent_id` (on nested blocks, naming the enclosing one), the block's own `{page, range, box}`, and the overlapping subset of its `atomic_grounding`. Two leaves carry no blocks, for different reasons: `null` means the field had no ranges, so nothing was quoted for it (the model synthesized the value or found none), while `[]` means valid ranges matched no block, which usually means the two inputs came from different parses. Handle them separately. A value inside a table matches both the `table` and its `table_cell`, so pick whichever your highlight needs (https://docs.landing.ai/dpt3/ground).

**Partial results (HTTP 206):** Parse sets `metadata.failed_pages` and per-page `status`; Extract sets `schema_violation_error` and `warnings`. Data is still returned and credits are consumed. **Errors:** every v2 error body has a stable `code` and a human-readable `message`; branch on `code`, never on message text. Credits are consumed only on 200/206; error responses are free, and async jobs bill only when they complete. Per-endpoint error tables: [parse-troubleshoot](https://docs.landing.ai/dpt3/parse-troubleshoot), [extract-troubleshoot](https://docs.landing.ai/dpt3/extract-troubleshoot).

## Processing Modes and Service Tiers (v2)

Every v2 request runs on a service tier, `standard` or `priority`. Jobs default to `standard` and accept `service_tier` to switch. The sync endpoints (`POST /v2/parse`, `POST /v2/extract`; `client.v2.parse` / `client.v2.extract` in the libraries) always run at `priority`, return the result inline, accept `save_to` / `saveTo`, and reject `service_tier` and `output_save_url`. Ground (`POST /v2/ground`) is synchronous only, with no Jobs variant or `service_tier` choice; its response always reports `billing.service_tier: "priority"`, but the call itself is always free.

| Mode | Best for | Result | Turnaround | Credits |
|------|----------|--------|-----------|---------|
| Jobs, `service_tier=standard` (default) | Work with no one waiting: automated pipelines, background agent steps, scheduled ingestion, the largest documents | Poll, or `output_save_url` | Minutes to hours | Half the `priority` rate |
| Jobs, `service_tier=priority` | Time-sensitive work sync can't handle: larger documents, or not holding a connection open | Poll, or `output_save_url` | Seconds to minutes | Full rate |
| Sync | Interactive work: a person is waiting on this one result | Inline in the response | Seconds to minutes | Full rate |

When you write a script or pipeline for a user, stay on `standard` jobs unless the user asks for faster turnaround; a script the user runs later is not interactive work. When they do ask, switch to `priority` jobs, which keep the batch and large-file handling. Use sync only for a single small document whose result the user needs in the same call, and pass `save_to` / `saveTo` so the full response lands on disk.

Sync requests and `priority` jobs share one per-minute page limit. A single document with more pages than that limit returns HTTP 429 on every attempt; retrying never helps, so submit it as a `standard` job, which takes up to 6,000 pages. Turnaround times are estimates, and rate limits depend on the pricing plan, so check https://docs.landing.ai/dpt3/sync-async and https://docs.landing.ai/dpt3/rate-limits for current values rather than assuming them.

Instead of polling, you can register a webhook endpoint (in the Playground settings; there is no management API) to receive signed `parse.succeeded`, `parse.failed`, `extract.succeeded`, and `extract.failed` events when jobs finish: https://docs.landing.ai/dpt3/webhooks.

Both job create endpoints accept `output_save_url` (a presigned URL where the result is delivered instead of the poll response; recommended with zero data retention). The URL must stay valid until the job completes, not just at submission: an expired or soon-expiring URL is rejected at creation with HTTP 422 and no credits consumed, so sign it with a validity that outlives the job. Under Zero Data Retention (https://docs.landing.ai/ade/zdr), a v2 job result is deleted as soon as you fetch it, and a never-fetched result is deleted 24 to 48 hours after the job completes; persist the first completed poll response immediately (as Core Flow does), because polling again returns HTTP 410 (`result_expired`). With `output_save_url`, the result is delivered to your storage, then deleted immediately. Guides: [parse-async](https://docs.landing.ai/dpt3/parse-async), [extract-async](https://docs.landing.ai/dpt3/extract-async). API reference: [parse jobs](https://docs.landing.ai/api-reference/parse/ade-parse-jobs), [extract jobs](https://docs.landing.ai/api-reference/extract/ade-extract-jobs).

## v1 APIs Without a v2 Equivalent

### Classify: Page-Level Classification

Assigns a class to each page of a raw document (not Parse output), so it composes with v2 pipelines: classify first, then route pages to parsing. Guide: https://docs.landing.ai/ade/ade-classify. API reference: [ade-classify](https://docs.landing.ai/api-reference/tools/ade-classify).

```bash
curl -X POST 'https://api.va.landing.ai/v1/ade/classify' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'document=@batch.pdf' \
  -F 'classes=[{"class":"invoice","description":"Commercial bill with line items and totals"},{"class":"bank_statement","description":"Monthly summary of account transactions"}]' \
  -F 'model=classify-latest'
```

The response lists one `class` per `page`. **Classify pages are 0-indexed (v1 convention); v2 Parse `options.pages` is 1-indexed, so add 1 when routing pages.** Unmatched pages come back as `unknown` with a `suggested_class`. Response fields: [ade-classify-response](https://docs.landing.ai/ade/ade-classify-response).

### Section: Table of Contents Generation

Generates a hierarchical table of contents. **Requires v1 Parse output** (it depends on the anchor tags only v1 Parse emits); do not feed it v2 Markdown. Guide: https://docs.landing.ai/ade/ade-section. API reference: [ade-section](https://docs.landing.ai/api-reference/tools/ade-section).

```bash
curl -X POST 'https://api.va.landing.ai/v1/ade/section' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'markdown=@v1-parse-output.md' \
  -F 'model=section-latest'
```

An optional `guidelines` field steers the hierarchy. Each entry has `title`, `level`, `section_number`, and `start_reference` (the v1 chunk id where the section begins). Response fields: [ade-section-response](https://docs.landing.ai/ade/ade-section-response).

### Build Extract Schema: Schema Generation

Generates or refines a JSON extraction schema from sample Markdown and/or a prompt; the returned schema string passes directly to either Extract API. Guide: https://docs.landing.ai/ade/ade-extract-schema-api. API reference: [ade-build-extract-schema](https://docs.landing.ai/api-reference/tools/ade-build-extract-schema).

```bash
curl -X POST 'https://api.va.landing.ai/v1/ade/extract/build-schema' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'markdowns=@sample.md' \
  -F 'model=extract-latest' \
  -F 'prompt=Extract the vendor name, invoice date, and total amount due'
```

### Split: Multi-Document Separation

Classifies and separates a file containing multiple documents (for example, a scanned packet of invoices and receipts). **Runs on v1 Parse output** (parse with `split=page` first). Guide: https://docs.landing.ai/ade/ade-split. API reference: [ade-split](https://docs.landing.ai/api-reference/tools/ade-split).

```bash
curl -X POST 'https://api.va.landing.ai/v1/ade/split' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'markdown=@v1-parse-output.md' \
  -F 'split_class=[{"name":"Bank Statement","description":"Summary of account activity over a period"},{"name":"Pay Stub","description":"Earnings and deductions for one pay period","identifier":"Pay Stub Date"}]' \
  -F 'model=split-latest'
```

A next-generation **Split v2 API is in Preview** (select partners; accepts Markdown from either Parse version): https://docs.landing.ai/splitv2/splitv2.

### v1 Parse and Extract (Superseded)

Still required for spreadsheets (XLSX, CSV), legacy binary Office files (DOC, PPT), password-protected files, custom figure prompts, and Section/Split pipelines. Same auth; host `api.va.landing.ai`; parse model `dpt-2-latest`; v1 responses use a flat `chunks` list with 0-indexed pages and `left/top/right/bottom` boxes. Guides: [v1 Parse](https://docs.landing.ai/ade/parse), [v1 Extract](https://docs.landing.ai/ade/ade-extract), [v1 Parse Jobs](https://docs.landing.ai/ade/ade-parse-async), [v1 Extract Jobs](https://docs.landing.ai/ade/ade-extract-async), [password-protected files](https://docs.landing.ai/ade/ade-parse-password), [custom figure prompts](https://docs.landing.ai/ade/ade-parse-custom-prompts), [chunk types](https://docs.landing.ai/ade/ade-chunk-types), [v1 JSON response](https://docs.landing.ai/ade/ade-json-response). File format support per version: [v2 file types](https://docs.landing.ai/dpt3/file-types), [v1 file types](https://docs.landing.ai/ade/ade-file-types).

## Workflow Rules

These rules come from field experience building document pipelines. Follow them whenever you write pipeline code around ADE. Full working pipelines (batch processing, classify-then-extract, RAG ingestion with vector databases, database loading, visualization, Streamlit review UIs) live at https://github.com/landing-ai/ade-sample-projects; most samples still use the v1 APIs, so check which endpoints a sample calls before borrowing code.

### Pre-Flight: Inspect Before You Code (mandatory)

Before writing any section-detection, heading-matching, or text-search code:

1. **Render 1 or 2 pages as PNG and read them as images.** Check: handwriting or scan versus digital text, single versus two-column layout, running headers or watermarks, whether headings are styled text or plain bold.
2. **Run one diagnostic parse on one sample** and inspect the Markdown head plus a block inventory (type, page, box, first characters of each block). Heading format is document-specific and cannot be inferred from the task description: `Introduction` may appear as `1. Introduction` (plain text), `## Introduction`, or `INTRODUCTION` inside a larger text block. Getting this wrong causes a silent zero-match failure.
3. **Cache parse output** (save the response JSON) and reuse it while developing; re-parse only when the document set changes. Parse 1 to 3 samples during development, never the full corpus.

If heading formats vary across documents, match sections with an Extract call instead of regex.

### Bounding-Box Work: Verify Visually (mandatory)

For any crop, overlay, or highlight built from grounding boxes:

- v2 `grounding.page` is 1-indexed; page renderers (such as PyMuPDF) are 0-indexed, so the renderer index is `page - 1`. An off-by-one lands the crop on an adjacent page.
- Boxes are normalized; multiply by the rendered page's pixel dimensions, not the PDF point size.
- **After producing the first output image, read it back as an image and describe what you see.** Compare against the request (asked for a table, got a chart? wrong page?). Only proceed with the remaining crops after the first is verified. A brightness heuristic cannot catch wrong-page bugs; visual inspection can.

### Multi-Page Tables (Stitching)

ADE may emit a table that spans pages as separate table blocks per page, and **any page's rows may come back as plain text instead of a table block**, not just the last page. Three approaches, in order of robustness:

1. **Parse, then Extract with a row schema** (an array-of-objects field): the extract model reads the full Markdown, so it absorbs format inconsistencies. Two API calls; use this by default.
2. **Parse the HTML tables from the Markdown**: one call, but fragile; requires uniform row structure and breaks silently if you switch tables to pipe syntax.
3. **`pandas.read_html` on the Markdown**: quick prototyping only; misses rows that were emitted as plain text.

After stitching, validate with domain checks: column totals match a stated total, running balances reconcile, dates are chronological, row counts match a stated count. These catch merge errors that no parser will flag.

### Classify-Then-Extract (Mixed Document Batches)

- **Each file is one document of unknown type:** run a one-field Extract (an enum `type` field) on the parsed Markdown, then extract again with the type-specific schema.
- **One file contains several documents:** use the v1 Split pipeline (v1 Parse with `split=page`, then Split, then Extract per sub-document).
- **Route pages before parsing:** use Classify on the raw file, then parse only the relevant pages with v2 `options.pages` (remember the 0-indexed to 1-indexed conversion).

### RAG and Embedding Granularity

Choose the embedding unit to match retrieval needs; ADE blocks are the finest unit but not always the right one:

| Level | Unit | Best for |
|-------|------|----------|
| Block | One ADE block | Tables, figures, forms with independent fields |
| Page | All blocks on a page | Slide decks, page-oriented documents |
| Section | Consecutive blocks grouped by heading | Narrative documents where answers span paragraphs |
| Document | Full Markdown or a summary | Classification, routing, coarse search |

Embed `text`, `table`, and `card` blocks; exclude `marginalia` (running headers, footers, page numbers). Carry grounding metadata (source file, page, box coordinates) into the vector store as columns so every retrieval hit traces back to a document location.

### Schema Design

- Start small (a few fields), then grow. Use descriptive field names and put format hints in descriptions ("in USD", "as YYYY-MM-DD"); descriptions act as extraction prompts.
- Keep nesting to one level of objects; use an array of objects for line items and transactions.
- v2 Extract **silently removes** unsupported JSON Schema keywords (`allOf`, `oneOf`, `const`, `pattern`, `maximum`, and others) instead of erroring; v1 Extract errors on them. Setting `strict=true` on v2 turns the silent removal into an HTTP 422. All fields are treated as required and nullable regardless of `required`.
- Schema authoring reference: https://docs.landing.ai/dpt3/ade-extract-schema-json. Or generate a starting schema with the Build Extract Schema API above.

### Batch Processing and Scale

- Batches run as `standard` jobs. Create every job first, record the `job_id`s, then wait on each; a create-then-wait loop per document serializes the batch on its slowest job.
- If you do fan out over sync endpoints, keep concurrency modest (single digits) and retry 429/5xx with exponential backoff, except a 429 on one oversized document, which no retry clears (see Processing Modes). Limits: https://docs.landing.ai/dpt3/rate-limits.
- A sync request that times out (HTTP 504) means the document is too large for sync; resubmit it as a job rather than retrying.
- Save every parse response to disk as you go; a re-run should never re-parse documents that already succeeded.

## Links

**Guides:**
- v2 overview: https://docs.landing.ai/dpt3/overview
- v2 quickstart: https://docs.landing.ai/dpt3/quickstart
- Parsing models (DPT-3 Pro vs. DPT-3 Verity): https://docs.landing.ai/dpt3/parse-models
- Ground extracted fields: https://docs.landing.ai/dpt3/ground
- Zero Data Retention: https://docs.landing.ai/ade/zdr
- Webhooks (job completion events): https://docs.landing.ai/dpt3/webhooks
- Migration guide (v1 to v2): https://docs.landing.ai/dpt3/migration-guide
- Rate limits: https://docs.landing.ai/dpt3/rate-limits
- Credit consumption: https://docs.landing.ai/dpt3/credit-consumption

**API reference (full contracts, with cURL/Python/Node tabs):**
- Parse v2: https://docs.landing.ai/api-reference/parse/ade-parse
- Parse Jobs v2: https://docs.landing.ai/api-reference/parse/ade-parse-jobs
- Extract v2: https://docs.landing.ai/api-reference/extract/ade-extract
- Extract Jobs v2: https://docs.landing.ai/api-reference/extract/ade-extract-jobs
- Ground v2: https://docs.landing.ai/api-reference/ground/ade-ground
- Classify: https://docs.landing.ai/api-reference/tools/ade-classify
- Section: https://docs.landing.ai/api-reference/tools/ade-section
- Build Extract Schema: https://docs.landing.ai/api-reference/tools/ade-build-extract-schema
- Split: https://docs.landing.ai/api-reference/tools/ade-split
- Parse v1: https://docs.landing.ai/api-reference/tools/ade-parse
- Extract v1: https://docs.landing.ai/api-reference/tools/ade-extract

**Troubleshooting (per endpoint):**
- Parse v2: https://docs.landing.ai/dpt3/parse-troubleshoot
- Extract v2: https://docs.landing.ai/dpt3/extract-troubleshoot
- Ground v2: https://docs.landing.ai/dpt3/ground-troubleshoot
- Classify: https://docs.landing.ai/ade/ade-classify-troubleshoot
- Section: https://docs.landing.ai/ade/ade-section-troubleshoot
- Split: https://docs.landing.ai/ade/ade-split-troubleshoot

**Libraries:**
- Python guide (v2): https://docs.landing.ai/dpt3/ade-python
- ade-python source: https://github.com/landing-ai/ade-python
- TypeScript guide (v2): https://docs.landing.ai/dpt3/ade-typescript
- ade-typescript source: https://github.com/landing-ai/ade-typescript
