The demo works. You send a PDF to GPT-4o, ask it to extract line items, and it returns something that looks like JSON. Then you run it on 500 invoices in staging and watch it hallucinate table rows, drop page-two data entirely, and return different field names on identical documents. By the time you get to 10,000 PDFs a day in production, the failure rate makes the whole approach unusable.
That's the prototype trap. Sending raw PDFs to a vision model gets you coherent prose, but it loses table structure, page coordinates, and element types. The output is nondeterministic, which means your downstream agent can't reliably route on field values, validate totals, or trigger conditional workflows. You end up with a pipeline that works in a notebook and breaks in a queue.
The delta between a demo and a deployable agentic document workflow is almost entirely in how you handle the document layer. The Document AI market is projected to grow from USD 14.66 billion in 2025 to USD 27.62 billion by 2030 at a 13.5% CAGR, and most of that investment is being made precisely because raw LLM extraction doesn't hold up under real operating conditions.
A dedicated, deterministic PDF API layer handles structural extraction and document operations, while the LLM handles reasoning and routing. This article shows you how to build that, specifically how to wire Foxit's PDF Services API into an agent pipeline that's auditable, composable, and designed to run at volume.
1. The Production Gap: Why LLMs Reading PDFs Break at Scale
LLM-based PDF extraction fails in production because vision models reconstruct document structure from visual patterns rather than reading it deterministically. At scale, this produces hallucinated table rows, token cost explosion, schema drift between runs, and no replayable audit trail. A better prompt won't fix any of these:
Hallucinated table rows: When a vision model interprets a multi-column PDF table as image input, it reconstructs structure from visual patterns. On clean PDFs, this is often accurate. On anything with merged cells, rotated text, or embedded watermarks, the model invents rows or merges adjacent data. At 10,000 invoices per day, a 2% hallucination rate is 200 wrong records, and you won't know which ones.
Token cost explosion: A 10-page PDF converted to images for vision input can consume 2,000–4,000 tokens per page depending on resolution. Processing 10,000 documents daily at that rate pushes monthly costs into the tens of thousands of dollars for the extraction step alone, before you've done any reasoning.
No structured output contract: The LLM returns whatever JSON shape seems plausible given the prompt. Field names drift between runs. Tables become arrays of strings on one call and arrays of objects on the next. Downstream validators fail silently because the schema wasn't enforced upstream.
No audit trail: If a contract clause gets misclassified and triggers the wrong workflow, you have no way to replay what the model saw and why it decided what it did. Under HIPAA or SOC 2, "the model just got it wrong" isn't an acceptable incident response.
A clean architectural boundary fixes all four. The PDF API handles structured extraction and document operations deterministically, and the LLM operates on the typed output rather than the raw document. That separation is what makes an agentic document workflow actually workable.
2. What an Agentic Document Workflow Actually Looks Like
An agentic document workflow (ADW) differs from RAG and traditional IDP by maintaining state across multi-step operations, coordinating extraction, retrieval, structured output, and downstream actions in a single orchestrated loop. The agent decides, acts, then decides again based on what the action produced.
LlamaIndex coined the term "Agentic Document Workflows" (ADW) to describe a pattern that goes beyond both RAG and traditional intelligent document processing. RAG retrieves text chunks and generates answers in one pass. Traditional IDP classifies and extracts, then routes to a rules-based engine. The agentic loop layers statefulness on top of both, which is what enables conditional routing, validation, and document generation inside the same pipeline.
The document lifecycle an agent must own in this model spans five stages, from ingest (OCR + structural parse) through extract (typed elements), reason (LLM decision layer), and act (generate, redact, sign, or archive), to emit (append an event trace for auditability). Each stage has a clear owner.
The mistake most teams make is mixing the LLM layer and the PDF API layer. The LLM should never see raw PDF bytes. It should receive typed JSON from the PDF API (table arrays, text blocks with bounding box coordinates, form field values) and make decisions on that structured data. The PDF API owns extraction. The LLM owns understanding.
3. The Core API Operations Agents Need
A production-ready PDF API layer for agentic workflows requires four capabilities, namely deterministic structural extraction that returns typed JSON, template-driven document generation, compliance-grade security operations (flatten, encrypt, redact), and programmatic eSign triggering with a native audit trail.
Structured extraction is the most important. The API should combine OCR, layout recognition, and AI parsing to return typed, element-classified JSON rather than raw text. Foxit's PDF Structural Extraction API extracts twelve different element types (text blocks, tables, form fields, images, and more) and returns them as structured JSON, making the output directly consumable by a downstream LLM or validation step without post-processing. The endpoint is currently in Trial status at schema v1.0.7, so pin your parsers to the version.schema field in the response. The schema can change between versions, and a breaking change will fail silently if you're not checking it.
Document generation: Agents that process documents also need to produce them. Foxit's DocGen API takes a base64-encoded DOCX template and a JSON data payload, merges them, and returns a PDF or DOCX. Before generation, the
Analyze Document (Base64)endpoint inspects the template's merge fields, which means you can validate that your data payload covers all required placeholders before sending the generation request, rather than discovering missing fields in the output.Security operations: Compliance archiving almost always requires flattening (merging annotations and form fields into a static layer), encryption, and sometimes redaction. These need to be callable as discrete API operations. The PDF Services API covers these as separate endpoints.
eSign triggering: If your workflow ends in a signature (contract approval, remittance confirmation, policy acknowledgment), the signing step needs to be a programmatic API call. The eSign API initiates signing workflows and maintains a native audit trail including signer identity, timestamps, and an activity history endpoint you can query. This is the one leg of the pipeline where Foxit provides audit data natively. The rest of the trace you build yourself (more on that in Section 5).
4. Wiring a PDF API into Your Agent: The REST Workflow Pattern
Every Foxit PDF Services API call follows a four-step async pattern (upload, submit, poll, download) that applies uniformly to extraction, OCR, compression, and flattening. Internalize this loop once and you can implement any operation without relearning the integration.
Prerequisites
To run the code in this section and the pipeline in Section 7, you need:
- Python 3.8+ with pip and a virtual environment
- The
requestslibrary - A code editor such as VS Code with the Python extension (PyCharm or any editor works too)
- A free Foxit developer account (sign up here, no credit card required) for your
client_idandclient_secret
Set up the workspace in one shot:
mkdir adw-pipeline && cd adw-pipeline
python3 -m venv .venv && source .venv/bin/activate
pip install requests
export FOXIT_CLIENT_ID="your_client_id"
export FOXIT_CLIENT_SECRET="your_client_secret"
The four-step loop
- POST to upload the source document as
multipart/form-datato receive adocumentId - POST to the operation endpoint (e.g., structural extraction) with the
documentIdto receive ataskId - GET to poll task status using the
taskIduntil status isCOMPLETEDorFAILED - GET to download the result by its
resultDocumentId(a ZIP holding the structural JSON for extraction, a processed PDF for everything else)
The full extraction loop against the https://na1.fusion.foxit.com base URL using requests:
import io
import json
import os
import time
import zipfile
import requests
BASE_URL = "https://na1.fusion.foxit.com/pdf-services/api"
HEADERS = {
"client_id": os.environ["FOXIT_CLIENT_ID"],
"client_secret": os.environ["FOXIT_CLIENT_SECRET"],
}
def upload_pdf(filepath: str) -> str:
"""Upload a PDF and return its documentId."""
with open(filepath, "rb") as f:
resp = requests.post(
f"{BASE_URL}/documents/upload",
headers=HEADERS,
files={"file": f},
)
resp.raise_for_status()
return resp.json()["documentId"]
def start_extraction(document_id: str) -> str:
"""Submit a structural extraction task and return the taskId."""
resp = requests.post(
f"{BASE_URL}/documents/pdf-structural-extract",
headers={**HEADERS, "Content-Type": "application/json"},
json={"documentId": document_id},
)
resp.raise_for_status() # 202 Accepted
return resp.json()["taskId"]
def poll_until_done(task_id: str, interval: int = 3, timeout: int = 120) -> str:
"""Poll task status until COMPLETED, then return the resultDocumentId."""
deadline = time.time() + timeout
while time.time() < deadline:
resp = requests.get(
f"{BASE_URL}/tasks/{task_id}",
headers=HEADERS,
)
resp.raise_for_status()
data = resp.json()
if data["status"] == "COMPLETED":
return data["resultDocumentId"]
if data["status"] == "FAILED":
raise RuntimeError(f"Extraction failed: {data}")
time.sleep(interval)
raise TimeoutError(f"Task {task_id} did not complete within {timeout}s")
def download_structure(result_document_id: str) -> dict:
"""Download the result ZIP and return the parsed structural JSON."""
resp = requests.get(
f"{BASE_URL}/documents/{result_document_id}/download",
headers=HEADERS,
)
resp.raise_for_status()
with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
return json.loads(zf.read("StructureInfo.json"))
def extract_pdf(filepath: str) -> dict:
doc_id = upload_pdf(filepath)
task_id = start_extraction(doc_id)
result_doc_id = poll_until_done(task_id)
return download_structure(result_doc_id)
In this code, you upload the source PDF to receive a documentId, submit the structural extraction task against documents/pdf-structural-extract (the API answers 202 Accepted with a taskId), poll the task endpoint every 3 seconds until the status flips to COMPLETED, then download the result document. The result arrives as a ZIP containing StructureInfo.json (the typed structural output) plus a rendered PNG of each page, so download_structure unpacks the archive in memory and returns the parsed JSON. A FAILED status surfaces as a RuntimeError instead of being silently retried.
For DocGen, the same upload-submit-poll-download shape applies conceptually, though the Base64 endpoint used in Section 7 is synchronous and returns the rendered document in the response body.
- Scaling the async model: The PDF Services API doesn't offer webhook callbacks. For high-volume workloads at 10,000+ PDFs per day, a poll loop that blocks your agent threads will become a bottleneck. Use a job queue (SQS, Celery, Redis Streams) where each worker owns the poll loop for its assigned task, then calls the agent's reasoning step only after the extraction result is available. The eSign API is the Foxit product with native webhook support for event-driven callbacks. PDF Services is async polling only.
5. Event-Sourced AI: Building an Auditable Agent Trace
Event sourcing solves the agentic pipeline audit problem by capturing every AI decision as an immutable, append-only log entry keyed to a stable document ID. Every processing step (extraction, validation, generation, signing) becomes a replayable event with typed inputs, the policy version that governed the decision, and the output.
Consider a failure mode that won't surface until you're in an audit. An agent misclassified a contract clause as non-binding, triggered the wrong approval workflow, and now you need to demonstrate exactly what inputs the agent saw and what policy it applied. Without that capture, you're reconstructing from logs and guessing.
Event sourcing captures every state change in a system as an immutable event appended to a log. For an agent pipeline, that means every AI decision step appends a record containing its inputs, the policy or prompt version used, and the output. The log is append-only and keyed by a stable document_id. You can replay any document's processing history, re-run a specific step with a changed policy, and produce a per-document audit trail on demand.
Foxit's task-based API architecture gives you clean event boundaries to log. Each operation returns a typed payload, a taskId, and a status, and those are the natural checkpoints where you append events. The eSign API contributes a native audit trail for the signing leg (signer identity, timestamps, and a queryable activity history endpoint), which is the one part of the pipeline you don't have to instrument yourself.
The rest of the trace is yours to build. A minimal event schema needs five fields:
-
document_id: the stable key every event is grouped under -
event_type: e.g.,extraction_completed,validation_passed,document_generated -
timestamp: when the event was appended -
payload: the API response or LLM output for the step -
policy_version: the prompt hash or policy ID that governed the decision
Write this to a Postgres table with id SERIAL, document_id UUID, event_type TEXT, payload JSONB, policy_version TEXT, created_at TIMESTAMPTZ DEFAULT NOW(). That's enough to replay any document through the pipeline and produce a per-event audit log.
A naive LLM pipeline leaves intermediate states opaque and non-replayable. A deterministic PDF API layer combined with an append-only event log links every decision to its source data so you can reconstruct it on demand. That's what makes HIPAA, SOC 2, or GDPR audits tractable.
6. MCP as the Agent-Native Integration Layer
The Model Context Protocol (MCP) exposes PDF API tools directly to LLM agent hosts (Claude Desktop, VS Code, Cursor) without custom wrapper code. Foxit's open-source MCP Server ships 35+ callable tools and authenticates via three environment variables, making it the fastest path from agent host to production PDF operations.
The Model Context Protocol (MCP) is an emerging open standard for exposing tool APIs to LLM-based agents. The agent host calls tools by name, the MCP server handles the underlying API calls, and typed results come back ready for the agent's context window, without custom wrapper code.
Foxit publishes an open-source MCP Server that exposes the PDF Services API as 35+ callable tools. It ships in Python (FastMCP framework, Python 3.11+, uv) and TypeScript (pnpm) variants, and authenticates via three environment variables (FOXIT_CLOUD_API_CLIENT_ID, FOXIT_CLOUD_API_CLIENT_SECRET, and FOXIT_CLOUD_API_HOST). Any MCP-compatible host (Claude Desktop, VS Code with GitHub Copilot, Cursor) can connect to it. If you're already using one of those hosts, you can call PDF tools without writing any wrapper code at all.
There's a genuine tradeoff, though. MCP eliminates custom integration work by adding a protocol hop and some latency overhead. For latency-sensitive, high-volume pipelines at the scale described in Section 4, direct REST calls with your own job queue are the right call. Use MCP when you want to iterate fast or when your agent host is already MCP-native and the overhead doesn't matter.
For full installation steps, the complete tool catalog, and host configuration for Claude Desktop and Cursor, see the dedicated guide, Foxit MCP Server: Give AI Agents Direct Access to 30+ PDF Tools via Model Context Protocol.
7. Build This Today: A Minimal Agentic Invoice Processing Pipeline
A production-ready agentic invoice processing pipeline takes six steps, starting from a free developer account and ending in an optional eSign approval, with every step appended to an audit log. It ingests a PDF invoice, extracts line items as structured JSON, validates totals with an LLM reasoning step, and generates a remittance PDF confirmation using the DocGen API. You can implement this against your own documents this week.
Step 1: Create a free Foxit developer account. The free Developer plan gives you 500 credits per year with no credit card required. Your Client ID and Client Secret are available immediately from the dashboard.
Step 2: POST your invoice PDF to the upload endpoint and capture the
documentId. If you don't have an invoice handy, use this sample invoice PDF, the same document the output below was extracted from.

The sample invoice the pipeline ingests. The line item table is what structural extraction returns as a typed table element in Step 3.
-
Step 3: POST to the PDF Structural Extraction endpoint with the
documentId. Poll untilCOMPLETED, then download the result ZIP and readStructureInfo.json(theextract_pdffunction from Section 4 does all of this). The JSON types every element it detects, so the invoice's line item table arrives as a structuredtableelement with typed cells, not as a block of text you then need to parse. Here is the output for the sample invoice, trimmed to one cell per concept:
{
"analyzeResult": {
"version": {
"schema": "1.0.7",
"software": "FoxitPDFAnalyzer",
"model": "idp-analysis"
},
"pages": [
{
"pageNumber": 1,
"size": { "width": 612, "height": 792, "unit": "point" },
"state": "success"
}
],
"elements": [
{
"type": "title",
"content": {
"text": "INVOICE",
"style": { "fontFamilyName": "Arial", "fontSize": 24.0 }
},
"region": { "page": 1, "boundingBox": [90, 71, 189, 71, 189, 99, 90, 99] },
"score": 0.88,
"id": "title1"
},
{
"type": "paragraph",
"content": { "text": "Invoice Number: INV-2025-0042" },
"region": { "page": 1, "boundingBox": [90, 187, 254, 187, 254, 200, 90, 200] },
"score": 0.87,
"id": "paragraph3"
},
{
"type": "table",
"content": {
"body": {
"rowCount": 7,
"columnCount": 5,
"cells": [
{
"paragraph": {
"type": "paragraph",
"content": { "text": "Description" },
"id": "paragraph7"
},
"rowIndex": 0,
"columnIndex": 1,
"rowSpan": 1,
"columnSpan": 1,
"region": { "page": 1, "boundingBox": [171, 286, 258, 286, 258, 301, 171, 301] }
},
{
"paragraph": {
"type": "paragraph",
"content": { "text": "$1,560.00" },
"id": "paragraph15"
},
"rowIndex": 1,
"columnIndex": 4,
"rowSpan": 1,
"columnSpan": 1,
"region": { "page": 1, "boundingBox": [430, 300, 517, 300, 517, 328, 430, 328] }
}
]
}
},
"regions": [{ "page": 1, "boundingBox": [82, 285, 519, 285, 519, 422, 82, 422] }],
"id": "table1"
}
]
}
}
Pin your parser to analyzeResult.version.schema. The schema is at v1.0.7 and the endpoint is in Trial. Version increments are your signal to check for breaking changes.
Step 4: Pass the table JSON to your LLM agent. The prompt becomes a structured data validation task ("do the line item totals equal the invoice total?") rather than an OCR interpretation task. The LLM operates on typed data, not image pixels.
Step 5: POST the validated JSON plus a DOCX remittance template to the DocGen
Generate Document (Base64)endpoint. A ready-made remittance template, verified end-to-end against the live API, matches the sample invoice's data.

The remittance template's merge tokens. Note that {{TableStart:line_items}} and {{TableEnd:line_items}} sit in the same table row, which is required for the loop to render. Run the Analyze Document (Base64) endpoint first to confirm your template's merge fields match your data payload, and keep the template under the endpoint's 4 MB post-base64 cap (compress embedded images in Word's Picture Format pane if you hit it). Receive the output PDF.

The remittance PDF generated from the template and the validated invoice data, rendered by the live DocGen API. Your Step 5 output should match this.
- Step 6 (optional): POST to the eSign API to route the remittance for approval. The API creates a signing folder, notifies signers, and maintains the activity history audit trail.
Every step in this pipeline produces a discrete event with a typed payload. Append each one to your audit log. You end up with a complete, replayable record of what was extracted, what the agent decided, and what was generated, for every document that passes through the system.
Frequently Asked Questions
What is an agentic document workflow, and how does it differ from RAG?
An agentic document workflow (ADW) maintains state across multi-step operations and coordinates document processing, retrieval, structured output, and downstream actions in a single orchestrated loop. RAG retrieves text and generates a response in one pass. An ADW agent decides, acts, and then decides again based on what each action produced, enabling conditional routing, validation, and document generation within the same pipeline.
Why does sending raw PDFs to a vision model fail at scale?
Vision models reconstruct document structure from visual patterns rather than reading it deterministically. At 10,000 documents per day, even a 2% hallucination rate produces 200 wrong records, and you won't know which ones. Field name drift between runs breaks downstream validators, and there's no replayable audit trail when a misclassification triggers the wrong workflow.
How does a dedicated PDF API layer fix the production delta?
A dedicated PDF API like Foxit's PDF Services API extracts structure deterministically, returning typed JSON with twelve element types, bounding boxes, and form field values, rather than reconstructing it from pixels. The LLM then operates on that typed output rather than raw bytes, making the extraction step auditable, schema-consistent, and cost-predictable.
What is the async polling pattern in the Foxit PDF Services API?
Every operation follows the same four-step loop. POST to upload the document (returns a documentId), POST to the operation endpoint (returns a taskId), GET to poll status until COMPLETED or FAILED, then GET to download the result by its resultDocumentId. This pattern applies uniformly across extraction, OCR, compression, and flattening.
When should I use the Foxit MCP Server instead of direct REST calls?
Use the MCP Server when you're already in an MCP-native host (Claude Desktop, Cursor, VS Code with Copilot) and want to call PDF tools without writing wrapper code. Use direct REST calls with a job queue (SQS, Celery, Redis Streams) for latency-sensitive, high-volume pipelines where the protocol hop overhead matters.
How do I build an auditable trace for an AI document pipeline?
Implement event sourcing, where every AI decision step appends an immutable record to an append-only log containing document_id, event_type, timestamp, payload, and policy_version. Foxit's task-based API provides clean event boundaries, and each taskId response is a natural checkpoint. The eSign API contributes a native audit trail for the signing leg. The rest you instrument yourself.
Does the Foxit PDF Structural Extraction API schema change between versions?
Yes. The endpoint is currently in Trial status at schema v1.0.7, and the schema can change between version increments. Always pin your parsers to the analyzeResult.version.schema field in the response and check the Trial API reference when you see a version bump, or a breaking change will fail silently.
Final Thoughts
The delta between a working demo and a production agentic document workflow comes down to one architectural decision. Does your PDF layer produce deterministic, typed output? A vision model reconstructing structure from pixels doesn't scale. A dedicated extraction layer does.
Foxit's PDF Services API, DocGen API, and eSign API cover the full document lifecycle (extraction, generation, security operations, and signing) through a consistent REST pattern your agents can call reliably at volume. Add an append-only event log on top of those clean API boundaries and you have the auditability story that compliance teams and incident response actually require.
To test the extraction-to-generation pipeline against your own documents, create a free Foxit developer account (500 credits, no credit card required). The full API reference is at developer-api.foxit.com.
What document type are you trying to automate in your current pipeline, and which step is giving you the most friction? Drop it in the comments.
Top comments (0)