Document Extraction (ADE)

SkillDocs & knowledge

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Document Extraction (ADE) skill

What this skill tells your AI

The instructions your AI receives, as published by landing-ai/ade-document-processing-skills in plugins/ade-document-processing/skills/document-extraction/SKILL.md and read by ahel’s review.

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.

APIVersionEndpointGuide
Parsev2POST https://api.ade.landing.ai/v2/parsehttps://docs.landing.ai/dpt3/parse
Parse Jobsv2POST/GET https://api.ade.landing.ai/v2/parse/jobshttps://docs.landing.ai/dpt3/parse-async
Extractv2POST https://api.ade.landing.ai/v2/extracthttps://docs.landing.ai/dpt3/extract
Extract Jobsv2POST/GET https://api.ade.landing.ai/v2/extract/jobshttps://docs.landing.ai/dpt3/extract-async
Groundv2POST https://api.ade.landing.ai/v2/groundhttps://docs.landing.ai/dpt3/ground
Classifyv1POST https://api.va.landing.ai/v1/ade/classifyhttps://docs.landing.ai/ade/ade-classify
Sectionv1POST https://api.va.landing.ai/v1/ade/sectionhttps://docs.landing.ai/ade/ade-section
Build Extract Schemav1POST https://api.va.landing.ai/v1/ade/extract/build-schemahttps://docs.landing.ai/ade/ade-extract-schema-api
Splitv1POST https://api.va.landing.ai/v1/ade/splithttps://docs.landing.ai/ade/ade-split
Parse (superseded)v1POST https://api.va.landing.ai/v1/ade/parsehttps://docs.landing.ai/ade/parse
Extract (superseded)v1POST https://api.va.landing.ai/v1/ade/extracthttps://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 byv2 Extractv1 Extractv1 Sectionv1 Split
Parse v2Yes (preferred; reads the embedded doc_id)NoNoNo (use v1 Parse for Split pipelines)
Parse v1Yes (works, but no doc_id link)YesYesYes

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) patternCurrent (v2)
chunks list in the responsestructure tree of pages and blocks; slice markdown with each block's grounding.range
0-indexed page numbersPages are 1-indexed in v2: grounding.page, metadata.failed_pages, options.pages
Box keys left/top/right/bottomRenamed xmin/ymin/xmax/ymax (still normalized 0 to 1)
model=dpt-2-latestmodel=dpt-3-pro-latest (pin a dated snapshot such as dpt-3-pro-20260710 in production)
confidence, low_confidence_spansRemoved 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)

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.

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
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")
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/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).

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

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
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)
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/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, 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.

ModeBest forResultTurnaroundCredits
Jobs, service_tier=standard (default)Work with no one waiting: automated pipelines, background agent steps, scheduled ingestion, the largest documentsPoll, or output_save_urlMinutes to hoursHalf the priority rate
Jobs, service_tier=priorityTime-sensitive work sync can't handle: larger documents, or not holding a connection openPoll, or output_save_urlSeconds to minutesFull rate
SyncInteractive work: a person is waiting on this one resultInline in the responseSeconds to minutesFull 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, extract-async. API reference: parse jobs, extract jobs.

v1 APIs Without a v2 Equivalent

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
65
Forks
17
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
document-extraction-landing-ai
Source
github.com/landing-ai/ade-document-processing-skills