The document renders fine in a PDF viewer. The moment your code tries to read it, you're looking at a flat string with no table structure, no reading order, and no way to tell which values belong to which labels.
PDFs dominate data-intensive workflows, including invoices, contracts, financial reports, and scanned intake forms. The format was designed for visual fidelity, not machine readability. Text extraction libraries can pull words off a page, but they discard the structure that makes those words useful, including which column a number belongs to, whether a line is a table header or a paragraph, and whether a field is a form input or surrounding prose.
This guide shows you how to solve that with a REST-based PDF data extraction API. You'll see the four-step request lifecycle, the practical difference between basic text extraction and AI-assisted structural extraction, and how to route the resulting JSON into a downstream system. Every request and response below comes from a live run against Foxit's PDF Services API, with examples in both shell and Python.
Why basic PDF parsing breaks down
PDFs don't have a single internal format. A word-processor export has a proper text layer. A scanned invoice is a bitmap with no text whatsoever. A multi-column research paper has text objects positioned on the page without semantic grouping. Form fields sit in a separate data structure from the body text around them.
Naive extraction libraries treat all of these the same way, walking the PDF content stream and concatenating text objects in order. The output is readable but structurally blind. A two-column table becomes a jumbled stream that interleaves values from the left and right columns. Reading order is lost. Headers are indistinguishable from body text. Form field labels merge with their values.
For simple use cases such as keyword search or full-text indexing, that's often acceptable. If you need to populate a database field from an invoice line item, compare figures across financial reports, or feed structured records into a retrieval augmented generation (RAG) pipeline, flat text output breaks the downstream system before it starts. Modern AI and business intelligence tools expect schema-consistent JSON, not raw strings.
How to choose the right extraction approach
Three broad approaches exist for getting structured data out of PDFs, covering rule-based, template-based, and AI-assisted structural extraction. Tools like Adobe PDF Extract API, Amazon Textract, Google Document AI, Nutrient, and Apryse all land somewhere on this spectrum.
Rule-based extraction uses regular expressions and positional heuristics, finding the number 10 pixels to the right of the label "Invoice Total" and returning it. This works well for identical, high-volume layouts from a single source (one vendor's purchase orders, for example) but breaks the moment a layout changes by even a few pixels.
Template-based extraction pre-defines field positions for known document types. It's more flexible than pure rules but still requires a template per document variant. Maintenance overhead climbs quickly when you're processing documents from dozens of suppliers.
AI-assisted structural extraction uses machine learning to classify content regions regardless of layout. It recognizes a block of cells as a table even when that table has merged headers, irregular spacing, or no visible grid lines. It handles documents with variable layouts without requiring per-template configuration, and it applies optical character recognition (OCR) to scanned documents that have no text layer at all.
For consistent, known layouts at high volume, rule-based or template-based approaches are viable and predictable. For mixed-layout documents, variable vendor formats, or content headed into AI or BI pipelines that expect typed JSON, AI-assisted structural extraction is the right default.
Prerequisites
Before running any of the code below, get the following in place.
- Runtime : Python 3.8 or later
- Package manager : pip, bundled with modern Python installs
- Environment isolation : venv, part of the standard library
- HTTP library : requests, installed with pip
- CLI tool : cURL for the raw HTTP examples
- Code editor : VS Code with the Python extension is a reasonable default, and PyCharm or Sublime Text work equally well
- Foxit developer account : sign up at app.developer-api.foxit.com/sign-up for a client ID and client secret, with no credit card required
Scaffold the workspace in one shot:
mkdir pdf-extraction && cd pdf-extraction
python3 -m venv .venv
source .venv/bin/activate
pip install requests
The commands above create a project directory, build an isolated virtual environment inside it, activate that environment so installs stay local to the project, and add the one third-party library the Python examples need.
API access and authentication setup
Once you're logged in, copy the client ID and client secret from the default application in the developer portal. Every request passes both values as lowercase headers, and there is no separate token exchange step, so a request is authenticated as soon as those two headers are present.

The sign-up page the CTA points at. The panel on the right is where the client ID and client secret come from, and the free tier is enough to complete this whole walkthrough.
export FOXIT_CLIENT_ID="your-client-id"
export FOXIT_CLIENT_SECRET="your-client-secret"
Exporting the credentials as environment variables keeps them out of your source files and out of your shell history if you set them in a profile. Every example below reads them from the environment rather than hard-coding a literal.
The base URL for every call in this guide is https://na1.fusion.foxit.com/pdf-services/api, and it stays constant across upload, extraction, polling, and download. The interactive API reference documents each endpoint's request and response shape if you need more detail than this walkthrough covers.
Foxit also provides an on-premise PDF SDK for air-gapped or offline environments, though this guide covers the cloud REST API only.
Calling the PDF data extraction API endpoints
Every extraction job runs asynchronously and follows four steps, covering upload, job start, polling, and download. There is no synchronous variant, because layout analysis on a multi-page document takes longer than a typical request timeout allows.

The four calls in order. Each one returns exactly the identifier the next one needs.
Each call returns exactly what the next one needs. An upload produces a documentId, starting a job produces a taskId, and a finished job produces a resultDocumentId.
Step 1: Upload the document
Send the PDF as multipart form data, with the file attached under the field name file. Use this sample invoice if you'd rather not supply your own document. It carries a five-column line-item table, which exercises both row and column indexing later on.

The source document. Notice that "API Integration Consulting" wraps onto two lines inside its cell, which matters once you start comparing cell text.
curl -X POST "https://na1.fusion.foxit.com/pdf-services/api/documents/upload" -H "client_id: $FOXIT_CLIENT_ID" -H "client_secret: $FOXIT_CLIENT_SECRET" -F "file=@invoice_table_test.pdf"
The command above posts the PDF as multipart form data and returns a JSON body containing a documentId. Capture that value, because every later call in the flow depends on it.
Step 2: Start the extraction job
Foxit provides two extraction endpoints, and the right one depends on what your pipeline consumes.
Basic extraction (POST /pdf-services/api/documents/modify/pdf-extract) returns flat text, image, or page-level output. It requires an extractType parameter, and the accepted values are TEXT, IMAGE, and PAGE, in uppercase. Sending a lowercase value returns a VALIDATION_ERROR rather than falling back to a default. Use this endpoint when you want raw text or image assets and your own code handles the parsing.
Structural extraction (POST /pdf-services/api/documents/pdf-structural-extract) applies OCR, layout recognition, and classification to sort content into twelve element types, covering title, head, paragraph, table, image, headerFooter, form, hyperlink, footnote, sidebar, annotation, and formula. It preserves reading order, spatial position, and table cell grids, including for scanned documents with no existing text layer.
curl -X POST "https://na1.fusion.foxit.com/pdf-services/api/documents/pdf-structural-extract" -H "client_id: $FOXIT_CLIENT_ID" -H "client_secret: $FOXIT_CLIENT_SECRET" -H "Content-Type: application/json" -d '{"documentId": "YOUR_DOCUMENT_ID"}'
This request starts the analysis job and returns HTTP 202 with a taskId rather than a finished result. The 202 is the signal that work has been queued, so treat any expectation of inline output as a bug in your integration rather than a slow response.
Step 3: Poll for completion
Poll the task endpoint until the status reaches a terminal value. Status strings are uppercase, and the sequence runs PENDING, IN_PROGRESS, then either COMPLETED or FAILED.
curl -X GET "https://na1.fusion.foxit.com/pdf-services/api/tasks/YOUR_TASK_ID" -H "client_id: $FOXIT_CLIENT_ID" -H "client_secret: $FOXIT_CLIENT_SECRET"
The response carries taskId, status, progress, and, once the work finishes, resultDocumentId. A three-second interval between polls is a sensible default for single-page documents.
Step 4: Download the result
curl -X GET "https://na1.fusion.foxit.com/pdf-services/api/documents/YOUR_RESULT_DOC_ID/download" -H "client_id: $FOXIT_CLIENT_ID" -H "client_secret: $FOXIT_CLIENT_SECRET" -o result.zip
The download returns application/zip. Inside are two kinds of file, StructureInfo.json holding the extracted content, and one PNG per detected table region. Despite a filename like page_p0.pdf_0.png, that image is a crop of the table rather than a render of the whole page.

The whole sequence against the live API. Note the 202 on the extract call, the COMPLETED status before any download is attempted, and the archive listing exactly two files.

The PNG that shipped in the ZIP for the invoice above, at 437 by 84 pixels. It shows the detected table only, which is a useful way to confirm the API found the region you expected.
Here is the shape of the JSON, trimmed to one element of each kind:
{
"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",
"id": "e1",
"score": 0.99,
"content": { "text": "INVOICE" },
"region": { "page": 1, "boundingBox": [72, 60, 240, 60, 240, 96, 72, 96] }
},
{
"type": "table",
"id": "e7",
"region": { "page": 1, "boundingBox": [48, 360, 812, 360, 812, 512, 48, 512] },
"content": {
"body": {
"rowCount": 4,
"columnCount": 5,
"cells": [
{ "rowIndex": 0, "columnIndex": 1, "rowSpan": 1, "columnSpan": 1,
"paragraph": { "content": { "text": "Description" } } },
{ "rowIndex": 1, "columnIndex": 1, "rowSpan": 1, "columnSpan": 1,
"paragraph": { "content": { "text": "API Integration\r\nConsulting" } } }
]
}
}
}
]
}
}
Three details in that payload decide whether your parsing code works. Every element sits under analyzeResult, so there is no top-level elements key to index. Element text lives at content.text rather than on the element itself. And region.boundingBox is an eight-number polygon describing four corner pairs, not a four-number rectangle, so unpacking it into x, y, width, height raises an error.

The same file from the run above, abbreviated. The highlighted keys are the four that parsing code gets wrong most often, and the eight numbers under boundingBox are what a four-value unpack trips over.
Processing structured JSON output in your pipeline
Tables are the part most likely to surprise you. A table element exposes content.body with rowCount, columnCount, and a flat cells array, where each cell carries its own rowIndex and columnIndex. There is no two-dimensional rows array and no separate headers array, so you build the grid yourself from those indexes.
import json
import zipfile
def clean(text):
"""Normalize cell text.
A label that wraps inside its cell arrives with a literal \\r\\n in the
middle of the string, so .strip() leaves it untouched. Splitting and
rejoining collapses any internal whitespace run into a single space.
"""
return " ".join(text.split())
def table_to_records(table):
body = table["content"]["body"]
grid = [["" for _ in range(body["columnCount"])] for _ in range(body["rowCount"])]
for cell in body["cells"]:
text = cell.get("paragraph", {}).get("content", {}).get("text", "")
grid[cell["rowIndex"]][cell["columnIndex"]] = clean(text)
header, *rows = grid
return [dict(zip(header, row)) for row in rows]
with zipfile.ZipFile("result.zip") as archive:
with archive.open("StructureInfo.json") as handle:
data = json.load(handle)
elements = data["analyzeResult"]["elements"]
tables = [el for el in elements if el["type"] == "table"]
paragraphs = [el for el in elements if el["type"] == "paragraph"]
for table in tables:
for record in table_to_records(table):
print(record)
In this code, you open the downloaded archive and read StructureInfo.json directly out of it without unzipping to disk, index into analyzeResult to reach the element list, then filter that list by type to separate tables from prose. For each table, table_to_records allocates an empty grid from the reported row and column counts, drops each cell into position using its own indexes, and finally zips the first row against the remaining rows to produce one dictionary per line item. Those dictionaries map directly onto database columns or a pandas DataFrame.
Running it against the sample invoice prints:
{'#': '1', 'Description': 'API Integration Consulting', 'Qty': '10', 'Unit Price': '$ 150.00', 'Line Total': '$1,500.00'}
{'#': '2', 'Description': 'Compliance Review', 'Qty': '5', 'Unit Price': '$ 200.00', 'Line Total': '$1,000.00'}
{'#': '', 'Description': '', 'Qty': '', 'Unit Price': 'Subtotal:', 'Line Total': '$2,500.00'}
Notice that Description reads "API Integration Consulting" on one line, because clean collapsed the wrapped \r\n that the source cell contained. Notice too that the subtotal row arrives as a normal row with empty leading cells, so a pipeline writing straight to a database should skip rows whose key columns are blank rather than assuming every row is a line item.
Filtering by type is the pattern that generalizes. Paragraph and title elements drop into a chunking function for a RAG pipeline, already separated by semantic type. Table records go to a warehouse or a BI dashboard. Each element also carries a score, and a low value is worth a manual check before you trust the result downstream.
Common mistakes and troubleshooting
Most failures in this flow come from a small set of predictable causes.
-
Reading
data["elements"]: there is no top-levelelementskey. The correct path isdata["analyzeResult"]["elements"], and skipping that level raises aKeyError. -
Unpacking
boundingBoxas four numbers : it is an eight-number polygon of four corner pairs, so code expectingx, y, width, heightraises aValueError. There is nobboxkey. -
Reading
table["rows"]ortable["headers"]: neither key exists. Build the grid fromrowIndexandcolumnIndexas shown above. -
Reading
labelandvaluefrom a form element : form elements do not carry those fields. Read the element's text and position instead. -
Comparing status in the wrong case : status values are uppercase, so a test against
"completed"never matches. -
Calling
.strip()on cell text : a wrapped label contains\r\nin the middle of the string, wherestriphas no effect. Normalize with" ".join(text.split()). -
Sizing a loop from
info.basicInfo.elementCounts: that summary and theelementsarray can disagree. On the sample invoice it reported 25 paragraphs whileelementsheld 5, so count the array you are about to iterate rather than trusting the metadata. -
Omitting
extractTypeon basic extraction :modify/pdf-extractrejects the request withVALIDATION_ERRORuntil you passTEXT,IMAGE, orPAGE. -
Downloading before the task reports
COMPLETED: polling once and assuming success yields aresultDocumentIdthat does not exist yet.
What this workflow handles end to end
The four-step PDF data extraction API workflow handles everything from a single invoice to thousands of mixed-layout reports processed overnight. Use basic extraction when you want raw text or images, and structural extraction when your pipeline needs typed JSON with preserved tables, reading order, and element classification. Processing a batch means looping the same sequence per file and tracking each taskId independently, so one slow document does not stall the rest.
On the compliance side, Foxit encrypts API traffic with TLS 1.2 or higher and protects documents at rest with AES-256, on SOC 2 Type II certified infrastructure. Support for HIPAA (through a business associate agreement), GDPR, and CCPA is documented on Foxit's API security and compliance page, which is worth reading before you route regulated documents through any hosted pipeline.
Create a free account at app.developer-api.foxit.com/sign-up and run the upload, extract, poll, and download sequence against one of your own documents. What extraction problems have you hit in your own pipelines? Drop them in the comments.
Top comments (0)