Programmatic PDF Editing with Foxit PDF Services API: Pages, Merges, Splits, and Flattening at Scale

Manually editing PDFs doesn’t scale when you’re processing hundreds of documents a day. This guide uses working Python and cURL examples to walk through the Foxit PDF Editor API’s core operations, covering page manipulation, merging, splitting, and flattening.
If you’re building document workflows at scale, you already know that manual PDF editing doesn’t cut it. Whether you’re generating contracts, processing invoices, or packaging reports, you need an API that handles page manipulation, merging, splitting, and flattening without breaking under load. Foxit PDF Services API gives you exactly that.
This guide covers the operations you’ll use most: adding and removing pages, merging documents, splitting by page range, and flattening annotations and form fields. Code examples are in Python, but the API is REST-based, so the patterns translate to any stack.
What Is the Foxit PDF Services API?
Foxit PDF Services API is a cloud-based REST API for programmatic PDF manipulation. You send documents and parameters; it returns processed PDFs. No local dependencies, no rendering engine to maintain.
The API handles:
Page insertion, deletion, and reordering
Document merging (combining multiple PDFs into one)
Document splitting (breaking one PDF into multiple outputs)
Flattening (converting annotations, form fields, and overlays into static page content)
Authentication uses API keys. Every request requires client_id and client_secret as separate HTTP request headers.
Why Backend PDF Editing Is an Infrastructure Decision
Teams processing hundreds of PDFs a day have an architecture problem, not a tooling problem.
When your CRM spits out contracts, your ERP generates invoices, and your intake forms produce patient records, the question is how you process these at scale without standing up a server farm or babysitting a library version matrix. At 500+ documents per day, manual desktop tooling is off the table. Per-file scripts that depend on a locally installed library become a liability the moment the library version shifts or a new language target appears.
Two architectural approaches dominate this space: SDK-based libraries and cloud REST APIs. SDK libraries require local installation, version pinning, language-specific bindings, and ongoing maintenance every time a dependency shifts. A cloud REST API requires none of that. Any language that can send an HTTP request can call it, with no package to install, no runtime to configure, and no compatibility matrix to manage.
This guide covers four operations in full: page manipulation (move, rotate, delete, add), merging multiple PDFs into one output, splitting a large document by page count, and flattening annotations into permanently static content. All of it runs via REST calls against the Foxit PDF Services API, authenticated with two request headers and structured around a single four-step loop that applies to every endpoint in the suite.
Prerequisites
Get your environment in order before any code runs. Each tool links to its canonical install page.
venv for project isolation
Node.js 18+ and npm
axiosfor Node examplesA code editor such as VS Code with the Python extension; alternatives include PyCharm, WebStorm, and Sublime Text
Postman (optional, for API exploration alongside the Foxit API Playground)
A Foxit developer account, available at account.foxit.com/site/sign-up with no credit card required and free credits included
Set up your workspace:
mkdir foxit-pdf-tutorial && cd foxit-pdf-tutorial
python3 -m venv .venv
source .venv/bin/activate
pip install requests
Authentication and the Upload → Task → Poll → Download Loop
Authentication
Foxit PDF Services API authenticates via headers. Every request must include client_id and client_secret as separate HTTP request headers, exactly as named: lowercase, underscored. They’re passed individually, as plain strings. Concatenating them, Base64-encoding them, or prefixing them with Bearer are all auth mistakes that look like they should work and don’t.
The base host for the North America environment is https://na1.fusion.foxit.com. Set it once as an environment variable and reuse it:
export BASE_URL="https://na1.fusion.foxit.com"
export CLIENT_ID="your_client_id"
export CLIENT_SECRET="your_client_secret" Every cURL request in this guide uses that pattern:
--header "client_id: $CLIENT_ID"
--header "client_secret: $CLIENT_SECRET"
In Python, read credentials from os.environ and never hardcode them:
import os
import time
import requests
BASE_URL = os.environ["BASE_URL"]
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
headers = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
} In Node.js:
const axios = require("axios");
const BASE_URL = process.env.BASE_URL;
const headers = {
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}; The Four-Step Loop
Every operation in this API follows the same four steps, regardless of endpoint.
POST /pdf-services/api/documents/uploadwith the file asmultipart/form-data. The response returns an uploaddocumentId.POSTto the relevant endpoint (manipulate, combine, split, or flatten) with the uploaddocumentIdin the request body. The response is HTTP 202 with ataskId.GET /pdf-services/api/tasks/{task-id}until thestatusfield readsCOMPLETED. The completed task response also carries aresultDocumentIdand aprogresspercentage. Task statuses are always uppercase:PENDING,PROCESSING,COMPLETED,FAILED.GET /pdf-services/api/documents/{resultDocumentId}/downloadto retrieve your result. The uploaddocumentIdfrom step one and theresultDocumentIdfrom step three are different identifiers. You download using theresultDocumentId. Swapping these is the most common 4xx error.
Use a-midsummer-nights-dream.pdf to follow along with a known-good file:
curl --location "$BASE_URL/pdf-services/api/documents/upload" \
--header "client_id: $CLIENT_ID" \
--header "client_secret: $CLIENT_SECRET" \
--form 'file=@"a-midsummer-nights-dream.pdf"' Response:
{
"documentId": "abc123-upload-id"
}
A reusable Python polling function with exponential backoff:
def poll_task(task_id: str) -> str:
url = f"{BASE_URL}/pdf-services/api/tasks/{task_id}"
delay = 2
while True:
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
status = data["status"]
if status == "COMPLETED":
return data["resultDocumentId"]
elif status == "FAILED":
raise RuntimeError(f"Task {task_id} failed: {data}")
time.sleep(delay)
delay = min(delay * 2, 30) Branch on "COMPLETED" and "FAILED" in uppercase. Lowercase strings will silently never match.
The async model is what makes this scalable. A 50-page merge or a batch of 200 splits doesn’t block your thread while it runs. You dispatch the operation call, collect the taskId, and poll on a sensible backoff schedule. At 500+ PDFs per day, that non-blocking pattern keeps your worker pool from saturating.
Page Manipulation: Move, Rotate, Delete, and Add Pages
All page operations go through a single endpoint: POST /pdf-services/api/documents/modify/pdf-manipulate.
{
"documentId": "<upload-document-id>",
"password": "optional",
"config": {
"operations": [{ "type": "OPERATION_TYPE" }]
}
} The config.operations array runs in order. Page indexing is 1-based and adjusts after each step. If you delete page 3 and then reference page 4 in the next operation, that reference points to what was originally page 5 before the delete. Keep that in mind when chaining operations in a single request.
MOVE_PAGES
MOVE_PAGES reorders pages within the document:
{
"type": "MOVE_PAGES",
"pages": [5, 6, 7],
"targetPosition": 1
} targetPosition is 1-based and must not exceed the document’s total page count.
ROTATE_PAGES
ROTATE_PAGES changes page orientation:
{
"type": "ROTATE_PAGES",
"pages": [1, 2, 3],
"rotation": "ROTATE_CLOCKWISE_90"
} Valid rotation values are ROTATE_0, ROTATE_CLOCKWISE_90, ROTATE_180, and ROTATE_COUNTERCLOCKWISE_90.
DELETE_PAGES
DELETE_PAGES removes specific pages:
{
"type": "DELETE_PAGES",
"pages": [8, 9]
} ADD_PAGES
ADD_PAGES appends blank pages to the end of the document:
{
"type": "ADD_PAGES",
"pageCount": 2
}
ADD_PAGES has no insert-at-position parameter; blank pages always go to the end. There’s also no REPLACE_PAGES operation in the current API. Page replacement requires a DELETE_PAGES call on the target pages followed by a separate merge step to bring in the replacement content.
A document intake pipeline for scanned medical records often receives pages in landscape orientation when the archive standard is portrait. The Python call below normalizes pages 1 through 3 of the uploaded file:
def rotate_pages(document_id: str) -> str:
url = f"{BASE_URL}/pdf-services/api/documents/modify/pdf-manipulate"
payload = {
"documentId": document_id,
"config": {
"operations": [
{
"type": "ROTATE_PAGES",
"pages": [1, 2, 3],
"rotation": "ROTATE_CLOCKWISE_90"
}
]
}
}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
return response.json()["taskId"] A legal document workflow that assembles exhibits and needs to move pages 5, 6, and 7 to the front before archiving uses this request body:
{
"documentId": "<upload-document-id>",
"config": {
"operations": [
{
"type": "MOVE_PAGES",
"pages": [5, 6, 7],
"targetPosition": 1
}
]
}
}
All four operation types return HTTP 202 with a taskId. Pass that ID to poll_task, wait for COMPLETED, then download the resultDocumentId.
Merging and Splitting PDFs
Merging Multiple PDFs with pdf-combine
The merge endpoint is POST /pdf-services/api/documents/enhance/pdf-combine.
The request body takes a required documentInfos array. Each entry is a source document object:
{
"documentInfos": [
{ "documentId": "<doc-id-1>" },
{ "documentId": "<doc-id-2>" },
{ "documentId": "<doc-id-3>" }
],
"config": {
"addBookmark": true,
"continueMergeOnError": false,
"retainPageNumbers": false
}
} The config object supports three keys:
addBookmarkgenerates one bookmark per source file in the merged output, useful when the reader needs to navigate by source.retainPageNumberspreserves original page-number labels from each source.continueMergeOnErroris the one that matters most in production. Set it tofalsefor any job where a single bad source must fail the entire batch. Set it totrueonly for best-effort pipelines where partial output is acceptable. Always set it explicitly rather than relying on defaults.
The following code assembles a monthly client report from three sources. Download input.pdf, second.pdf, and input_for_compare.pdf to run this end-to-end:
def upload_file(path: str) -> str:
url = f"{BASE_URL}/pdf-services/api/documents/upload"
with open(path, "rb") as f:
response = requests.post(url, headers=headers, files={"file": f})
response.raise_for_status()
return response.json()["documentId"]
def merge_pdfs(document_ids: list) -> str:
url = f"{BASE_URL}/pdf-services/api/documents/enhance/pdf-combine"
payload = {
"documentInfos": [{"documentId": doc_id} for doc_id in document_ids],
"config": {
"addBookmark": True,
"continueMergeOnError": False,
"retainPageNumbers": False
}
}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
return response.json()["taskId"]
# Upload all three source files, collect their documentIds
doc_ids = [
upload_file("input.pdf"),
upload_file("second.pdf"),
upload_file("input_for_compare.pdf")
]
# Merge and download
merge_task_id = merge_pdfs(doc_ids) # POST combine → taskId
merge_result_id = poll_task(merge_task_id) # poll → resultDocumentId
Once poll_task returns a resultDocumentId, pass it to your download function.
Splitting a PDF by Page Count with pdf-split
The split endpoint is POST /pdf-services/api/documents/modify/pdf-split. The pdf- prefix is required and matches the naming convention used by pdf-combine, pdf-manipulate, and pdf-flatten.
The request body is minimal:
{
"documentId": "<upload-document-id>",
"pageCount": 10
} pageCount specifies how many pages go into each output file. The last file gets whatever pages remain. The current API exposes only this page-count split mode. File-size-based and bookmark-based splitting are not in the published reference.
curl --location "$BASE_URL/pdf-services/api/documents/modify/pdf-split" \
--header "client_id: $CLIENT_ID" \
--header "client_secret: $CLIENT_SECRET" \
--header 'Content-Type: application/json' \
--data '{
"documentId": "<upload-document-id>",
"pageCount": 10
}' The response returns a taskId. When polling reaches COMPLETED, the result contains multiple output files, one per split chunk.
Flattening Annotations: Making PDF Changes Permanent
Flattening merges annotations, form fields, and layers into the page content itself. The output is a single static PDF where no markup can be toggled, filled, or removed. This is a one-way transformation with no undo.
The endpoint is POST /pdf-services/api/documents/modify/pdf-flatten.
The request body is intentionally minimal:
{
"documentId": "<upload-document-id>"
} No flags for selective annotation or form-field handling are exposed in the current API reference. The operation flattens both annotations and form fields together in a single pass.
When to Flatten a PDF
Flattening is required in three situations:
Archiving a signed form. Without flattening, a technically capable recipient can still manipulate form fields in most PDF viewers, even after signature. Flattening removes that possibility entirely.
Print production. Annotation layers render differently across print drivers and produce visible artifacts in the final output. Flattening eliminates the variable.
Compliance workflows under HIPAA or legal hold. Documents at rest must be immutable. A live form field fails that requirement.
curl --location "$BASE_URL/pdf-services/api/documents/modify/pdf-flatten" \
--header "client_id: $CLIENT_ID" \
--header "client_secret: $CLIENT_SECRET" \
--header 'Content-Type: application/json' \
--data '{ "documentId": "<upload-document-id>" }' Flattening does not encrypt the document or apply password protection. If your workflow requires both, chain a pdf-protect call after flattening using POST /pdf-services/api/documents/security/pdf-protect, passing the resultDocumentId from the completed flatten task as the documentId input for the protect call and the password under config.userPassword. The result of one operation becomes the input of the next.
{
"documentId": "<flatten-result-document-id>",
"config": {
"userPassword": "<password>"
}
} In this body, you point documentId at the flatten task’s resultDocumentId and set the open password under config.userPassword. The password lives inside the config object, not at the top level. A top-level password key is accepted by the request but the task then fails with a parameter error, so nest it under config.
Building a Multi-Step PDF Editing Pipeline for High Volume
Chaining Operations with resultDocumentId
Every completed task returns a resultDocumentId. That ID becomes the documentId for the next operation. The full chain: upload, then operation A, then poll until A reaches COMPLETED, then operation B using A’s resultDocumentId, then poll until B reaches COMPLETED, then download B’s resultDocumentId.
A complete flatten-then-protect pipeline, with inline comments showing which ID type is active at each step:
import os
import time
import requests
BASE_URL = os.environ["BASE_URL"]
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
headers = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}
def upload_file(path: str) -> str:
url = f"{BASE_URL}/pdf-services/api/documents/upload"
with open(path, "rb") as f:
response = requests.post(url, headers=headers, files={"file": f})
response.raise_for_status()
return response.json()["documentId"]
def poll_task(task_id: str) -> str:
url = f"{BASE_URL}/pdf-services/api/tasks/{task_id}"
delay = 2
while True:
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
status = data["status"]
if status == "COMPLETED":
return data["resultDocumentId"]
elif status == "FAILED":
raise RuntimeError(f"Task failed: {data}")
time.sleep(delay)
delay = min(delay * 2, 30)
def flatten(document_id: str) -> str:
url = f"{BASE_URL}/pdf-services/api/documents/modify/pdf-flatten"
response = requests.post(url, json={"documentId": document_id}, headers=headers)
response.raise_for_status()
return response.json()["taskId"]
def protect(document_id: str, password: str) -> str:
url = f"{BASE_URL}/pdf-services/api/documents/security/pdf-protect"
payload = {"documentId": document_id, "config": {"userPassword": password}}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
return response.json()["taskId"]
def download(document_id: str, output_path: str):
url = f"{BASE_URL}/pdf-services/api/documents/{document_id}/download"
response = requests.get(url, headers=headers)
response.raise_for_status()
with open(output_path, "wb") as f:
f.write(response.content)
# Pipeline: upload → flatten → protect → download
upload_doc_id = upload_file("signed-form.pdf") # documentId
flatten_task_id = flatten(upload_doc_id) # taskId
flatten_result_id = poll_task(flatten_task_id) # resultDocumentId
protect_task_id = protect(flatten_result_id, os.environ["PDF_PASSWORD"]) # taskId
protect_result_id = poll_task(protect_task_id) # resultDocumentId
download(protect_result_id, "final-protected.pdf") Three distinct IDs flow through this pipeline: the upload documentId, the taskId returned by each operation call, and the resultDocumentId returned by each completed task. Confusing any two of these produces a 4xx.
Batch Polling for High-Volume Jobs
For a single document, sequential polling works fine. For a batch of thousands, polling each task one at a time becomes the bottleneck. The pattern that scales is to dispatch first, then consolidate.
Upload all source files in parallel and collect their documentIds. Dispatch all operation calls in parallel and collect their taskIds. Then run a single consolidated polling loop that iterates over all taskIds, checks each status, and branches on COMPLETED or FAILED per task. Your thread never blocks waiting for one document while others are already done, and it processes results as they arrive.
The multi.py sample in the foxitsoftware/developerapidemos repository demonstrates this exact dispatch-then-consolidate shape, including the resultDocumentId handoff across operations.
Common Mistakes and Troubleshooting
These are the integration errors that come up most often.
Polling too aggressively. Start at a two-second interval and double it up to a ceiling of around 30 seconds. Tight-looping the /tasks/{task-id} endpoint burns rate-limit budget and produces 429s that slow the whole pipeline down.
Treating FAILED as transient. A FAILED status means the job encountered a specific error. Read the task response body, surface the reason, and branch on it. Retrying indefinitely on FAILED produces the same failure indefinitely.
Downloading before COMPLETED. Some integrations skip the status check and call the download endpoint immediately after the operation response. The result is a partial or empty file. Always confirm status == "COMPLETED" before hitting the download endpoint.
Mixing up the three IDs. All three appear in the same code block so they can’t be confused:
upload_doc_id = upload_file("input.pdf") # documentId → input to operation endpoints
task_id = flatten(upload_doc_id) # taskId → input to the polling endpoint
result_doc_id = poll_task(task_id) # resultDocumentId → input to the download endpoint
download(result_doc_id, "output.pdf") The download endpoint takes the resultDocumentId, not the upload documentId or the taskId.
Task status casing. The polling endpoint returns PENDING, PROCESSING, COMPLETED, and FAILED in uppercase. Branching on lowercase "completed" will silently never match and your loop will run forever.
continueMergeOnError semantics. Setting this to true lets the merge skip a failing source and continue. Setting it to false aborts the entire batch if any source fails. In production, always set this explicitly.
Retrying 4xx responses. Retry on 5xx errors and timeouts. A 400 or 401 will return the same 4xx until you fix the request, so don’t retry them.
Auth header format. client_id and client_secret are separate headers, lowercase with underscores, passed individually. They’re not concatenated, Base64-encoded, or Bearer-prefixed.
pdf-flatten does not encrypt. Flattening makes a document’s content static but doesn’t restrict access to the file. If your compliance workflow requires both, chain a pdf-protect call after the flatten step using the flatten task’s resultDocumentId as the next documentId input.
PDF Editor API FAQ
What is the Foxit PDF Services API used for?
The Foxit PDF Services API is a cloud-based REST API for programmatic PDF manipulation at scale, covering page manipulation, document merging, splitting, and annotation flattening. Any language that can send HTTP requests can use it without installing local dependencies.
How do you merge multiple PDFs using the Foxit API?
Upload each source file to get a documentId, then POST all documentIds in a documentInfos array to POST /pdf-services/api/documents/enhance/pdf-combine. Poll the returned taskId until status is COMPLETED, then download the resultDocumentId.
What is PDF flattening and when should you use it?
PDF flattening converts interactive elements (form fields, annotations, and overlays) into static page content that cannot be edited. Use it when archiving signed forms, preparing documents for print production, or meeting compliance requirements such as HIPAA that mandate immutable records at rest.
What is the difference between documentId and resultDocumentId in the Foxit API?
documentId is returned after uploading a file and is used as input to operation endpoints. resultDocumentId is returned when a task reaches COMPLETED status and is used to download the processed file. They are different identifiers; using one where the other is expected produces a 4xx error.
Does the Foxit PDF Services API support splitting PDFs by custom page ranges?
The current pdf-split endpoint splits by a fixed pageCount value, producing equal-sized chunks with the last file containing remaining pages. File-size-based and bookmark-based splitting are not available in the published API reference.
How should you handle errors in the Foxit PDF Services API?
Retry on 5xx errors and timeouts using exponential backoff. Do not retry 400 or 401 responses, since those indicate a problem with the request itself. Read the response body on FAILED task status and branch on the specific error rather than retrying blindly.
Getting Started with Foxit PDF Services API
Create a free developer account. No credit card required, and free credits are included.
Once you have your credentials, open the Foxit API Playground. Start with pdf-combine. Download input.pdf and second.pdf, upload both to get two documentIds, POST the merge request with the JSON body from the Merging section, and poll for the result. The full cycle, authentication through download, takes under ten minutes on a first attempt.
If you’re evaluating this for production volume, check the current plan options and credit pools. Plans range from the free developer tier up through Startup and Business options with larger shared-credit allocations.
For the authoritative reference on every endpoint and parameter covered in this guide, the Foxit PDF Services API documentation is your starting point. The GitHub samples repository has complete Python and Node.js examples you can clone and run immediately, including the multi-step pipeline pattern this guide covers.
Foxit MCP Server: Give AI Agents Direct Access to 30+ PDF Tools via Model Context Protocol

Learn how the Foxit MCP Server lets AI agents handle PDF conversion, OCR, merge, signing, and document workflows.
Building a document automation agent with raw REST calls means writing the same boilerplate every time: upload a file, poll for task completion, download the result, handle errors, and manage auth tokens across multiple endpoints. For PDF operations, that loop repeats for every conversion, OCR call, or merge operation in your pipeline. The Foxit PDF API MCP Server collapses those loops into 30+ directly callable tools, with the MCP Server handling upstream REST complexity internally.
This guide covers how the server registers, what it exposes, how Foxit’s eSign and DocGen REST APIs extend the same agent session into signing and document generation workflows, and a concrete four-step workflow you can replicate against your own documents.
MCP Architecture in 90 Seconds
The MCP specification defines three roles. The Host is the LLM runtime (Claude Desktop, VS Code with GitHub Copilot, or Cursor) that manages the conversation and decides when to call tools. The Server is the capability provider, a process that advertises tools over the MCP protocol and executes them against some underlying service. Tools are the individual callable operations each server exposes, defined by a JSON schema the host uses to understand inputs and outputs.
Foxit occupies both sides of this architecture. Foxit PDF Editor ships as an MCP Host, the first PDF application to do so, connecting outward to external MCP servers like Gmail or Salesforce so its AI assistant can reach those services. The Foxit PDF API MCP Server works in the other direction, exposing Foxit’s cloud PDF Services API as 30+ tools for any MCP Host to call.
The MCP Server exposes PDF Services operations covering conversion between formats, content extraction, OCR, merge, split, compress, flatten, linearize, compare, watermark, form data import/export, security, and property inspection. Foxit’s eSign API and DocGen API are separate REST services that are not part of the MCP Server, so they are not exposed as MCP tools. A single agent workflow can still reach them, but through the agent’s own code-execution layer rather than through the MCP protocol, a distinction the eSign section explains in detail. The MCP tools handle PDF processing, while code the agent runs handles signing and template generation.

Prerequisites and Configuration
You need three things before registering the server:
- A Foxit developer account (free plan at developer-api.foxit.com, no credit card required) to obtain a
client_idandclient_secret - Python 3.11+ with the
uvpackage manager (or Node.js 18+ withpnpmfor the TypeScript version) - An MCP-compatible host such as Claude Desktop, VS Code, or Cursor
Clone the repo from github.com/foxitsoftware/foxit-pdf-api-mcp-server, then register it in your host’s MCP config. The walkthrough below uses Claude Desktop, but the same command, args, and env values work in any MCP host. In Claude Desktop, open Settings, select the Developer tab, and click Edit Config.

Then open claude_desktop_config.json with any text edito(stored at ~/Library/Application Support/Claude/ on macOS or %APPDATA%\Claude\ on Windows).

Add the Foxit server under the mcpServers key:
{
"mcpServers": {
"foxit-pdf": {
"command": "uv",
"args": [
"--directory",
"/path/to/foxit-pdf-api-mcp-server",
"run",
"foxit-pdf-api-mcp-server"
],
"env": {
"FOXIT_CLOUD_API_HOST": "https://na1.fusion.foxit.com/pdf-services",
"FOXIT_CLOUD_API_CLIENT_ID": "your_client_id",
"FOXIT_CLOUD_API_CLIENT_SECRET": "your_client_secret"
}
}
}
} Set FOXIT_CLOUD_API_CLIENT_ID and FOXIT_CLOUD_API_CLIENT_SECRET as environment variables on your system before the host process launches. Passing credentials through prompt context is a security risk your production setup should address. The client_id and client_secret from your developer portal authenticate all MCP tool calls to the PDF Services API. Adding eSign to the same agent session requires its own OAuth2 token exchange (covered in the next section), keeping the two credential scopes isolated.
After saving, completely quit and reopen Claude Desktop so it loads the config and launches the server as a local subprocess over standard input and output, the transport the Foxit server uses.

On restart, you should see the foxit MCP as Running in the local MCP servers in the developer tab. If you go the Customize tab then open the Connectors and click foxit-pdf to see the tools that the Foxit MCC has access to, you should see the 30+ tools registered.

If the connector never appears, the server failed to launch, and Claude’s logs at
~/Library/Logs/Claude/mcp*.logusually point to the cause, commonly a missinguvbinary or a wrong--directorypath.
To call a tool, type a natural-language request such as “Convert this Word file to PDF and compress it.” The agent selects pdf_from_word and pdf_compress, and Claude Desktop shows an approval prompt with the exact tool name and arguments before each call runs; the tool’s JSON result then streams back into the conversation.

That per-call approval is your audit point, since it surfaces precisely which tool the agent chose and what it passed.

If you would rather run the server in VS Code, the equivalent entry goes in .vscode/mcp.json under a top-level servers key, with an added "type": "stdio" field so VS Code launches the server the same way:
{
"servers": {
"foxit-pdf": {
"type": "stdio",
"command": "uv",
"args": [
"--directory",
"/path/to/foxit-pdf-api-mcp-server",
"run",
"foxit-pdf-api-mcp-server"
],
"env": {
"FOXIT_CLOUD_API_HOST": "https://na1.fusion.foxit.com/pdf-services",
"FOXIT_CLOUD_API_CLIENT_ID": "your_client_id",
"FOXIT_CLOUD_API_CLIENT_SECRET": "your_client_secret"
}
}
}
} You can also run MCP: Add Server from the Command Palette (Cmd+Shift+P or Ctrl+Shift+P), choose Command (stdio), and pick Workspace to write the entry into .vscode/mcp.json or Global to store it in your user profile. Once saved, VS Code shows inline Start, Stop, and Restart actions above the server entry and lists it under the MCP SERVERS – INSTALLED view, where a green indicator and the discovered tool count confirm the connection.
PDF Services MCP Tools: Full Catalog
The 30+ tools organize into seven functional categories. Most tools expect a documentId returned by a prior upload_document call, and return a resultDocumentId you pass to download_document when you want the output locally. The exception is pdf_from_url, which accepts a URL directly.
Document Lifecycle
upload_document: upload a PDF, Office file, image, HTML file, or plain text file; returns adocumentIdfor subsequent operationsdownload_document: retrieve a processed result to a local file pathdelete_document: clean up stored files from cloud storage
PDF Creation (file to PDF)
pdf_from_word,pdf_from_excel,pdf_from_ppt: convert Office documents to PDFpdf_from_text,pdf_from_image,pdf_from_html: convert plaintext, image files, or HTML to PDFpdf_from_url: fetch a live URL and convert the rendered page to PDF
PDF Conversion (PDF to file)
pdf_to_word,pdf_to_excel,pdf_to_ppt: extract editable Office formats from a PDFpdf_to_text,pdf_to_html,pdf_to_image: export text, HTML, or image representations
Manipulation
pdf_merge: combine multiple PDFs into onepdf_split: split by page ranges, page count, or every page individuallypdf_extract: pull a subset of pages from a PDFpdf_compress: reduce file size by 30-70% depending on content typepdf_flatten: convert form fields and annotations to static content (required for compliance archiving workflows)pdf_linearize: optimize for Fast Web View so browsers can stream PDF pages incrementallypdf_watermark: apply text or image watermarks with configurable position, opacity, and rotationpdf_manipulate: rotate, delete, or reorder pages
Analysis
pdf_compare: diff two PDFs and return a color-coded annotation document showing changespdf_ocr: convert scanned or image-based PDFs to searchable text with multi-language supportpdf_structural_analysis: detect document structure (titles, headings, paragraphs, tables with cell grids, images, form fields, hyperlinks, and metadata) with bounding boxes, following the Foxit PDF structural extraction engine schema. The result is JSON packaged inside a downloadable ZIP, not a return of named business entities; it reports layout and structure, and turning that into fields like party names is the job of the agent’s LLM, which performs the semantic extraction over that JSON
Security and Forms
pdf_protect: add password protection with 128-bit or 256-bit AES encryption and granular permission flagspdf_remove_password: strip password protection from a documentexport_pdf_form_data: extract form field values as JSONimport_pdf_form_data: populate form fields from a JSON payload
Properties
get_pdf_properties: return page count, page dimensions, PDF version, encryption status, digital signature info, embedded files, font inventory, and document metadata
The most-used operation in production document pipelines is pdf_from_word. Your agent uploads a DOCX file, gets back a documentId, then calls pdf_from_word with that ID. The underlying PDF Services API runs the conversion asynchronously, but the MCP Server handles polling internally and delivers the final result directly to your agent.
MCP tool call:
{
"name": "pdf_from_word",
"input": {
"documentId": "doc_abc123"
}
} MCP tool response:
{
"success": true,
"taskId": "task_xyz789",
"resultDocumentId": "doc_result456",
"message": "Word document converted to PDF successfully. Download using documentId: doc_result456"
} Pass doc_result456 to download_document to write the output PDF to disk, or feed it directly into another tool call like pdf_structural_analysis or pdf_compress as the next step in a chain.
Extending to eSign: Foxit’s Signing API as a Complementary REST Layer
After PDF processing via MCP tools, the next stage of the workflow dispatches a document for signature through Foxit’s eSign REST API, which lives at https://na1.foxitesign.foxit.com. This guide uses the na1 (US) region throughout.
Foxit also operates regional eSign hosts for the EU (
eu1.foxitesign.foxit.com), Canada (na2.foxitesign.foxit.com), and Australia (au1.foxitesign.foxit.com). The endpoints and payloads are identical; only the host changes, so pick the host that matches your data residency requirements.
The eSign API is not part of the Foxit MCP Server, so it is not an MCP tool, and that distinction matters for how the agent reaches it. Most MCP hosts cannot make arbitrary HTTP calls on their own, so the agent does not reach eSign “through MCP.” Instead, the agent invokes eSign from its own code-execution layer, whether that is a code interpreter the host provides, an agent framework that runs Python, or a custom tool you register that wraps the eSign calls. The cleanest production pattern is to wrap the eSign operations you need as custom MCP tools so the host calls them the same way it calls the PDF tools; the production considerations section returns to this. The code below is what that layer runs.
Authentication uses OAuth2 client_credentials. The eSign token exchange is a distinct flow from the PDF Services header auth that backs your MCP tools:
import requests
resp = requests.post(
"https://na1.foxitesign.foxit.com/api/oauth2/access_token",
data={
"client_id": ESIGN_CLIENT_ID,
"client_secret": ESIGN_CLIENT_SECRET,
"grant_type": "client_credentials",
"scope": "read-write"
}
)
access_token = resp.json()["access_token"] The Foxit eSign API developer guide uses “folder” terminology throughout. The key endpoints in an automated signing flow are:
POST /api/folders/createfolder: create a signing folder from one or more PDF documents, with signers, subject, and messagePOST /api/folders/sendDraftFolder: dispatch a draft folder to its signersPOST /api/templates/createtemplate: save a reusable template from a PDF with pre-placed signature fields (instantiate a folder from it later viaPOST /api/templates/createFolder)GET /api/folders/viewActivityHistory?folderId={id}: retrieve the activity audit trail for a folder once it has been sent (a draft that has never been shared returns an error)- Webhook channels for status callbacks: register a callback URL to receive real-time events when signers view, sign, or decline
A createfolder call takes the PDF output from your MCP pipeline, uploaded to eSign’s document storage after download_document retrieves it, and sets up the signing workflow:
POST /api/folders/createfolder
Authorization: Bearer {access_token}
Content-Type: application/json
{
"folderName": "Acme Corp Contract - Q3 2025",
"sendNow": false,
"fileUrls": ["https://your-storage.example.com/acme_contract_final.pdf"],
"fileNames": ["acme_contract_final.pdf"],
"parties": [
{
"firstName": "John",
"lastName": "Smith",
"emailId": "[email protected]",
"permission": "FILL_FIELDS_AND_SIGN",
"sequence": 1
}
]
} Set sendNow to false to create a draft folder, then dispatch it with a separate call to /api/folders/sendDraftFolder. Alternatively, set sendNow to true to create and send in a single call. For files not accessible via URL, add "inputType": "base64" and pass the documents as a base64FileString array instead of fileUrls; omitting inputType makes the API reject the base64 payload as empty.
Foxit’s eSign API ships with HIPAA, eIDAS, ESIGN Act, UETA, 21 CFR Part 11, FERPA, and FINRA compliance built in. Audit trail records carry signer location, IP address, recipient identity, event timestamp, consent confirmation, security level, and complete folder history. For legal defensibility in regulated industries, capture and store these fields in your own data layer, because relying solely on Foxit’s folder history API for compliance record-keeping introduces a single point of failure in your audit chain.
End-to-End Workflow: AI Agent Automates a Sales Contract
Picture a sales ops agent that starts from a single natural language goal, “Generate a contract for Acme Corp, $48,000 ARR, and send it to [email protected] for signature.” Nothing about the tool sequence is hard-coded. The MCP Server advertises its PDF tools to the host on connection, so the agent can read the goal, recognize that it has a template to render and a document to route for signature, and decide which operations to call and in what order. The PDF steps run as MCP tool calls; the DocGen and eSign steps run from the agent’s code layer. The sequence below is one plausible run the agent might choose, not a fixed script you wire up in advance.

To get a PDF to work with, the agent first reaches for MCP tools. It calls upload_document with the DOCX contract template, receives documentId: "doc_abc", and calls pdf_from_word. The MCP Server handles the async conversion internally and returns resultDocumentId: "doc_pdf" once it completes.
Needing to know what is inside that PDF, the agent calls pdf_structural_analysis with documentId: "doc_pdf". The tool does not hand back named entities like “party” or “ARR.” It returns a resultDocumentId pointing to a ZIP archive, so the agent calls download_document to retrieve it, unzips it, and reads the structural JSON, which describes headings, paragraphs, and table cells with their positions. The agent’s LLM is what performs the semantic extraction: it reads the structural JSON and pulls “Acme Corp” out of a heading or a contract value out of a table cell, confirming the fields it needs are present. The tool hands back structure; the model turns that structure into meaning. If you want the API to return business entities directly rather than leaning on the model to interpret layout, that is the job of Foxit’s iDox.ai Document API, a separate service built for entity and PII extraction.
With the field values in hand, the agent generates the finished contract through the DocGen API, posting to /document-generation/api/GenerateDocumentBase64 with the values merged into the template via {{dynamic_tags}} syntax. DocGen is synchronous, so the call returns the finalized PDF in the response body, with Acme Corp’s name, the $48,000 ARR figure, and the correct dates populated. No polling step is involved.
Finally, the agent routes the document for signature. It authenticates against the eSign OAuth2 endpoint, uploads the DocGen output, creates a signing folder via /api/folders/createfolder with [email protected] as the signer, and dispatches it via /api/folders/sendDraftFolder.
What ties this together is that the model decides the order from the goal, not a script. The PDF steps resolve to MCP tool calls the host already knows about; the DocGen and eSign steps run through the agent’s code layer, since those APIs are not MCP tools. The agent chains the output of one step into the input of the next, and the only orchestration you maintain is whatever exposes that code layer to the model, ideally a set of custom tools rather than ad hoc scripting.
Production Considerations: Error Handling, Rate Limits, and Data Governance
When you call PDF Services through the MCP Server, async polling happens inside the server process. Your agent receives a final resultDocumentId only after the task completes. When you call the raw PDF Services REST API directly, every operation returns a taskId you poll manually. The pattern below applies exponential backoff with a ceiling of 10 seconds per interval and a 30-second total timeout:
import time, requests
API_HOST = "https://na1.fusion.foxit.com/pdf-services"
auth_headers = {
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}
def poll_task(task_id: str, max_wait: int = 30) -> str:
delay = 1
elapsed = 0
while elapsed < max_wait:
resp = requests.get(
f"{API_HOST}/api/tasks/{task_id}",
headers=auth_headers
)
data = resp.json()
if data["status"] == "COMPLETED":
return data["resultDocumentId"]
time.sleep(delay)
elapsed += delay
delay = min(delay * 2, 10)
raise TimeoutError(f"Task {task_id} timed out after {max_wait}s") Because eSign and DocGen are not MCP tools, decide deliberately how the agent reaches them. Letting the model emit raw HTTP from a free-form code interpreter is brittle and hard to audit. The more durable pattern is to wrap the specific eSign and DocGen operations you use, such as create-folder, send-folder, and generate-document, as custom MCP tools with typed inputs. The host then calls them through the same protocol it uses for the PDF tools, the credentials stay in the tool process rather than in the prompt, and the agent’s choices become inspectable tool calls instead of opaque scripts.
The output of pdf_structural_analysis deserves its own caution. The structural JSON for a long contract can run to many thousands of elements, and feeding the entire file into the model can quietly blow past its context window, which tends to surface as a truncated or confused extraction rather than a clean error. Have the code that unzips the archive filter the JSON before the model sees it, keeping only the element types and pages that matter (for a contract, usually the heading blocks and the relevant table), rather than passing the whole document through.
The free developer plan at developer-api.foxit.com covers development and testing volumes. Production workloads above the free-tier threshold require a volume plan requested through the Developer Portal.
For data governance, all API traffic runs over TLS 1.2+, and documents at rest use AES-256 encryption. Foxit’s API security documentation covers SOC 2 Type II audit status, HIPAA BAA support, GDPR, CCPA, eIDAS, ESIGN Act, UETA, 21 CFR Part 11, FERPA, and FINRA requirements. Customer data runs in logically segmented environments. For healthcare, legal, or financial services pipelines, confirm your data residency requirements before connecting production document flows, then choose the matching regional eSign host noted earlier, since the host you call determines where data is processed.
PDF API MCP Server FAQs
What is the Foxit PDF API MCP Server?
The Foxit PDF API MCP Server is an open-source Model Context Protocol server that exposes Foxit’s cloud PDF Services API as 30+ callable tools. Any MCP-compatible AI agent host, including Claude Desktop, VS Code with GitHub Copilot, and Cursor, can invoke these tools directly.
What PDF operations does the Foxit MCP Server support?
The server supports conversion (Word, Excel, PowerPoint, image, HTML, and URL to PDF and back), OCR, merge, split, extract, compress, flatten, linearize, watermark, compare, form data import/export, password protection, and full document property inspection across seven functional tool categories.
How does the Foxit MCP Server handle authentication?
PDF Services tools authenticate via a client_id and client_secret set as environment variables before the MCP host launches. The eSign API uses a separate OAuth2 client_credentials token exchange against https://na1.foxitesign.foxit.com/api/oauth2/access_token. The two credential scopes are isolated by design.
Does the Foxit MCP Server work with Claude Desktop and VS Code?
Yes. The server registers using a standard mcp.json config block for VS Code with GitHub Copilot or a claude_desktop_config.json block for Claude Desktop. The same config structure works for Cursor. All three hosts discover the server’s tools automatically on connection.
Is the Foxit PDF API MCP Server free to use?
The Foxit developer account is free with no credit card required and covers development and testing volumes. Production workloads above the free-tier threshold require a volume plan through the Developer Portal.
Run Your First Tool Call Now
Getting a working MCP tool call takes under 15 minutes:
Create a free developer account at developer-api.foxit.com (no credit card, instant access). Copy your
client_idandclient_secretfrom the dashboard.Set the three environment variables:
export FOXIT_CLOUD_API_HOST="https://na1.fusion.foxit.com/pdf-services"
export FOXIT_CLOUD_API_CLIENT_ID="your_client_id"
export FOXIT_CLOUD_API_CLIENT_SECRET="your_client_secret" - Clone the repo, register it using the config block from the Prerequisites section, restart your MCP host, and invoke
pdf_from_urlwith any public URL. You’ll have a confirmed PDF output in your working directory. The Developer Portal also includes a live API Playground for validating request payloads against the PDF Services API before wiring them into an agent.
For a full signing workflow, the minimum viable addition to the MCP setup is authenticating against the eSign OAuth2 endpoint and posting to /api/folders/createfolder with a static PDF. DocGen field population, pdf_structural_analysis extraction, and webhook callbacks extend the same pattern incrementally from there.
Get your free API access at developer-api.foxit.com.
Generate Dynamic PDFs from JSON using Foxit APIs

See how easy it is to generate PDFs from JSON using Foxit’s Document Generation API. With Word as your template engine, you can dynamically build invoices, offer letters, and agreements—no complex setup required. This tutorial walks through the full process in Python and highlights the flexibility of token-based document creation.
Generate Dynamic PDFs from JSON using Foxit APIs
One of the more fascinating APIs in our library is the Document Generation API. This document generation API lets you create dynamic PDFs or Word documents using your own data as templates. That may sound simple – and the code you’re about to see is indeed simple – but the real power lies in how flexible Word can be as a template engine. This API could be used for:
- Creating invoices
- Creating offer letters
- Creating dynamic agreements (which can integrate with our eSign API)
All of this is made available via a simple API and a “token language” you’ll use within Word to create your templates. Whether you’re feeding in data from a database, a form submission, or a JSON API response, the process looks the same from your Python script. Let’s take a look at how this is done.
Credentials
Before we go any further, head over to our developer portal and grab a set of free credentials. This will include a client ID and secret values – you’ll need both to make use of the API.
Don’t want to read all of this? You can also follow along by video:
Using the API
The Document Generation API flow is a bit different from our PDF Services APIs in that the execution is synchronous. You don’t need to upload your document beforehand or download a result. You simply call the API (passing your data and template) and the result has your new PDF (or Word document). With it being this simple, let’s get into the code.
Loading Credentials
My script begins by loading in the credentials and API root host via the environment:
CLIENT_ID = os.environ.get('CLIENT_ID')
CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
HOST = os.environ.get('HOST') As always, try to avoid hard coding credentials directly into your code.
Calling the API
The endpoint only requires you to pass the output format, your data, and a base64 version of your file. “Your data” can be almost anything you like—though it should start as an object (i.e., a dictionary in Python with key/value pairs). Beneath that, anything goes: strings, numbers, arrays of objects, and so on.
Here’s a Python wrapper showing this in action:
def docGen(doc, data, id, secret):
headers = {
"client_id":id,
"client_secret":secret
}
body = {
"outputFormat":"pdf",
"documentValues": data,
"base64FileString":doc
}
request = requests.post(f"{HOST}/document-generation/api/GenerateDocumentBase64", json=body, headers=headers)
return request.json() And here’s an example calling it:
with open('../../inputfiles/docgen_sample.docx', 'rb') as file:
bd = file.read()
b64 = base64.b64encode(bd).decode('utf-8')
data = {
"name":"Raymond Camden",
"food": "sushi",
"favoriteMovie": "Star Wars",
"cats": [
{"name":"Elise", "gender":"female", "age":14 },
{"name":"Luna", "gender":"female", "age":13 },
{"name":"Crackers", "gender":"male", "age":13 },
{"name":"Gracie", "gender":"female", "age":12 },
{"name":"Pig", "gender":"female", "age":10 },
{"name":"Zelda", "gender":"female", "age":2 },
{"name":"Wednesday", "gender":"female", "age":1 },
],
}
result = docGen(b64, data, CLIENT_ID, CLIENT_SECRET) You’ll note here that my data is hard-coded. In a real application, this would typically be dynamic—read from the file system, queried from a database, or sourced from any other location.
The result object contains a message representing the success or failure of the operation, the file extension for the result, and the base64 representation of the result. To turn that base64 string back into a file, decode it first:
b64_bytes = result["base64FileString"].encode('ascii')
binary_data = base64.b64decode(b64_bytes) Most likely you’ll always be outputting PDFs, so here’s a simple bit of code that stores the result:
with open('../../output/docgen_sample.pdf', 'wb') as file:
file.write(binary_data)
print('Done and stored to ../../output/docgen_sample.pdf') There’s a bit more to the API than I’ve shown here so be sure to check the docs, but now it’s time for the real star of this API, Word.
Using Word as a Template
I’ve probably used Microsoft Word for longer than you’ve been alive and I’ve never really thought much about it. But when you begin to think of a simple Word document as a template, all of a sudden the possibilities begin to excite you. In our Document Generation API, the template system works via simple “tokens” in your document marked by opening and closing double brackets.
Consider this block of text:
See how name is surrounded by double brackets? And food and favoriteMovie? When this template is sent to the API along with the corresponding values, those tokens are replaced dynamically. In the screenshot, notice how favoriteMovie is bolded. That’s fine. You can use any formatting, styling, or layout options you wish.
That’s one example, but you also get some built-in values as well. For example, including today as a token will insert the current date, and can be paired with date formatting to specify how the date looks:
Remember the array of cats from earlier? You can use that to create a table in Word like this:
Notice that I’ve used two new tags here, TableStart and TableEnd, both of which reference the array, cats. Then in my table cells, I refer to the values from that array. Again, the color you see here is completely arbitrary and was me making use of the entirety of my Word design skills.
Here’s the template as a whole to show you everything in context:
The Result
Given the code shown above with those values, and given the Word template just shared, once passed to the API, the following PDF is created:
What About Converting PDF to JSON?
So far we’ve been going one direction: JSON data in, PDF out. But what if you need to go the other way—extract structured content from a PDF and work with it in your application?
Foxit’s PDF Services API includes an Extract endpoint that handles exactly this. You upload a PDF, specify whether you want TEXT, IMAGE, or PAGE-level data, and the API returns the extracted content. The text output is particularly useful if you want to feed the result into a data pipeline, search index, or AI workflow.
Here’s a quick look at how extraction works in Python. First, upload your PDF:
def uploadDoc(path, id, secret):
headers = {
"client_id":id,
"client_secret":secret
}
with open(path, 'rb') as f:
files = {'file': (path, f)}
request = requests.post(f"{HOST}/pdf-services/api/documents/upload", files=files, headers=headers)
return request.json()
doc = uploadDoc("../../inputfiles/input.pdf", CLIENT_ID, CLIENT_SECRET) Then call the Extract endpoint with the document ID and the type of content you want. The result comes back in a structured format you can parse, store, or pass along to other tools—including an LLM if you’re building an AI document pipeline.
You can read a full walkthrough in our PDF text extraction guide.
Ready to Try?
If this looks cool, be sure to check the docs for more information about the template language and API. Sign up for some free developer credentials and reach out on our developer forums with any questions.
If you’re building AI agents or LLM-powered workflows, Foxit also offers an MCP server that lets you connect your agents directly to Foxit PDF Services—so your AI tools can generate, extract, and process documents without any custom glue code.
Want the code? Get it on GitHub (Python).
If you are more of a Node person, check out that version. Get it on GitHub (Node.js).
Building Auditable, AI-Driven Document Workflows with Foxit APIs

We had an incredible time at API World 2025 connecting with developers, sharing ideas, and seeing how Foxit APIs power everything from AI-driven resume builders to interactive doodle apps. In this post, we’ll walk through the same hands-on workflow Jorge Euceda demoed live on stage—showing how to build an auditable, AI-powered document automation system using Foxit PDF Services and Document Generation APIs.
How to Build an AI Resume Analyzer with Python & Foxit APIs (API World 25′)
This year’s API World was packed with energy—and it was amazing meeting so many developers face-to-face at the Foxit booth. We spent three days trading ideas about document automation, AI workflows, and integration challenges.
Our team hosted a hands-on workshop and sponsored the API World Hackathon, where developers submitted 16 high-quality projects built with Foxit APIs. Submissions ranged from:
Automated legal-advice generators
Compatibility-rating apps that analyze your personality match
AI-powered resume optimizers that tailor your CV to dream-job descriptions
Collaborative doodle games that turn drawings into shareable PDFs
Each project offered a new perspective on what’s possible with Foxit APIs—and we loved seeing the creativity.
Among all the sessions, Jorge Euceda’s workshop stood out as a crowd favorite. It showed how to make AI document decisions auditable, explainable, and replayable using event sourcing and two key Foxit APIs. That’s exactly what we’ll walk through below.
Replicate the Full Demo
Click here to grab the project overview file.
Prefer to follow along with the live session instead of reading step-by-step?
Watch Jorge’s complete “AI-Powered Resume to Report” presentation from API World 2025.
It includes every step shown below—plus real-time API responses.
What You’ll Build
A complete, auditable workflow:
Resume Upload → Extract Resume Data → AI Candidate Scoring → Generate HR Report → Event Store
This workshop is designed for technical professionals and managers who want to learn how to use application programming interfaces (APIs) and explore how AI can enhance document workflows. Attendees will get hands-on experience with Foxit’s PDF Services (extraction/OCR) and Document Generation APIs, and see how event sourcing turns AI decisions into an auditable, replayable ledger.
By the end, you’ll have a Python-based demo that extracts data from a PDF resume, analyzes it against a policy, and generates a polished HR Report PDF with a traceable event log.
Getting Set Up
To follow along, you’ll need:
Access to a terminal with a Python 3.9+ Environment and internet connectivity
Visual Studio Code or your preferred IDE
Basic familiarity with REST/JSON (helpful but not required)
- Install Dependencies
python -V
# virtual environment setup, requests installation
python3 -m venv myenv
source myenv/bin/activate
pip3 install requests - Download the project’s zip file below
Now extract the files somewhere in your computer, open in Visual Studio Code or your preferred IDE.
You may use any sample resume PDF for inputs/input_resume.pdf. A sample one is provided, but you may leverage any resume PDF you wish to generate a report on.
- Create a Foxit Account for credentials
Create a Free Developer Account now or navigate to our getting started guide, which will go over how to create a free trial.
Hands-On Walkthrough
Step 1 – Open the Project
Now that you’ve downloaded the workshop source code, navigate to the resume_to_report.py file, which will serve as our main entry point.
Once dependencies are installed and the ZIP file extracted, open your workspace and run:
python3 resume_to_report.py You should see console logs showing:
An AI Report printed as JSON
A generated PDF (
outputs/HR_Report.pdf)An event ledger (
outputs/events.json) with traceable actions
Step 2 — Inspect the outputs
Open the generated HR report to review:
Candidate name and phone
Overall fit score
Matching skills & gaps
Summary and policy reference in the footer
Then open events.json to see your audit trail—each entry captures the AI’s decision context.
{
"eventType": "DecisionProposed",
"traceId": "8d1e4df6-8ac9-4f31-9b3a-841d715c2b1c",
"payload": {
"fitScore": 82,
"policyRef": "EvaluationPolicy#v1.0"
}
} This is your audit trail.
Step 3 — Replay & Explain a Policy Change
Replay demonstrates why event-sourcing matters:
Edit
inputs/evaluation_policy.json: add a hard requirement (e.g.,"kubernetes") or adjust the job_description emphasis.Re-run the script with the same resume.
Compare:
New decision and updated PDF content
Event log now reflects the updated rationale (
PolicyLoadedsnapshot → newDecisionProposedwith the sametraceIdlineage)
Emphasize: The input resume hasn’t changed; only policy did — the event ledger explains the difference.
Policy: Drive Auditable & Replayable Decisions
The AI assistant uses a JSON policy file to control how it scores, caps, and summarizes results. Every policy snapshot is logged as its own event, creating a replayable audit trail for governance and compliance.
{
"policyId": "EvaluationPolicy#v1.0",
"job_description": "Looking for a software engineer with expertise in C++, Python, and AWS cloud services. Experience building scalable applications in agile teams; familiarity with DevOps and CI/CD.",
"overall_summary": "Make the summary as short as possible",
"hard_requirements": ["C++", "python", "aws"]
} Notes:
policyIdappears in both the report and event log.job_descriptiondefines what the AI is looking for.Changing these values creates a new traceable event.
Generate a Polished Report
Next, use the Foxit Document Generation API to fill your Word template and create a formatted PDF report.
Open inputs/hr_report_template.docx, you will find the following HR reporting template with placeholders for the fields we will be entering:
Tips:
Include lightweight branding (logo/header) to make the generated PDF presentation-ready.
Include a footer with traceable Policy ID and Trace ID Events
Results and Audit Trail
Here’s what the final HR Report PDF looks like:
Every decision has a Trace ID and Policy Ref, so you can recreate the report at any time and verify how the AI arrived there.
Why Event-Sourced AI Matters
This pattern does more than score resumes—it proves that AI decisions can be transparent, deterministic, and trustworthy.
By using Foxit APIs to extract, analyze, and generate documents, developers can bring auditability to any workflow that relies on machine logic.
Key Takeaways
Auditability – Every AI step emits a verifiable event.
Replayability – Change a policy and regenerate for deterministic results.
Explainability – Decisions carry policy and trace references for clear “why.”
Automation – PDF Services and Document Generation handle the document lifecycle end-to-end.
Try It Yourself
Ready to build your own auditable AI workflow?
Demo and Source Code: document-workflows-with-foxit.pages.dev
Foxit Developer Portal: developer-api.foxit.com
API Docs: docs.developer-api.foxit.com
Watch the Full Presentation: Euceda’s API World session
Closing Thought
At API World, we set out to show how Foxit APIs can power real, transparent AI workflows—and the community response was incredible. Whether you’re building for HR, legal, finance, or creative industries, the same pattern applies:
Make your AI explain itself.
Start with the Foxit APIs, experiment with policies, and turn every AI decision into a traceable event that builds trust.
How to Chain PDF Actions with Foxit

Performing a single action with the Foxit PDF Services API is straightforward, but what’s the best way to handle a sequence of operations? Instead of downloading and re-uploading a file for each step, you can chain actions together by passing the output of one job as the input for the next. This tutorial walks you through a complete Python example of how to build an efficient document optimization workflow that compresses and then linearizes a PDF.
How to Chain PDF Actions with Foxit
When working with Foxit’s PDF Services, you’ll remember that the basic flow involves:
- Uploading your document to Foxit to get an ID
- Starting a job
- Checking the job
- Downloading the result
This is handy for one off operations, for example, converting a Word document to PDF, but what if you need to do two or more operations? Luckily this is easy enough by simply handing off one result to the next. Let’s take a look at how this can work.
Credentials
Remember, to start developing and testing with the APIs, you’ll need to head over to our developer portal and grab a set of free credentials. This will include a client ID and secret values you’ll need to make use of the API.
If you would rather watch a video (or why not both?) – you can watch the walkthrough below:
Creating a Document Optimization Workflow
To demonstrate how to chain different operations together, we’re going to build a basic document optimization workflow that will:
- Compress the document by reducing image resolution and other compression algorithims.
- Linearize the document to make it better viewable on the web.
Given the basic flow described above, you may be tempted to do this:
- Upload the PDF
- Kick off the Compress job
- Check until done
- Download the compressed PDF
- Upload the PDF
- Kick off the Linearize job
- Check until done
- Download the compressed and linearized PDF
This wouldn’t require much code, but we can simplify the process by using the result of the compress job—once it’s complete—as the source for the linearize job. This gives us the following streamlined flow:
- Upload the PDF
- Kick off the Compress job
- Check until done
- Kick off the Linearize job
- Check until done
- Download the compressed and linearized PDF
Less is better! Alright, let’s look at the code.
First, here’s the typical code used to bring in our credentials from the environment, and define the Upload job:
import os
import requests
import sys
from time import sleep
CLIENT_ID = os.environ.get('CLIENT_ID')
CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
HOST = os.environ.get('HOST')
def uploadDoc(path, id, secret):
headers = {
"client_id":id,
"client_secret":secret
}
with open(path, 'rb') as f:
files = {'file': f}
request = requests.post(f"{HOST}/pdf-services/api/documents/upload", files=files, headers=headers)
return request.json() def compressPDF(doc, level, id, secret):
headers = {
"client_id":id,
"client_secret":secret,
"Content-Type":"application/json"
}
body = {
"documentId":doc,
"compressionLevel":level
}
request = requests.post(f"{HOST}/pdf-services/api/documents/modify/pdf-compress", json=body, headers=headers)
return request.json()
def linearizePDF(doc, id, secret):
headers = {
"client_id":id,
"client_secret":secret,
"Content-Type":"application/json"
}
body = {
"documentId":doc
}
request = requests.post(f"{HOST}/pdf-services/api/documents/optimize/pdf-linearize", json=body, headers=headers)
return request.json() Note that the compressPDF method takes a required level argument that defines the level of compression. From the docs, we can see the supported values are LOW, MEDIUM, and HIGH.
Now, two more utility methods – one that checks the task returned by the API operations above and one that downloads a result to the file system:
def checkTask(task, id, secret):
headers = {
"client_id":id,
"client_secret":secret,
"Content-Type":"application/json"
}
done = False
while done is False:
request = requests.get(f"{HOST}/pdf-services/api/tasks/{task}", headers=headers)
status = request.json()
if status["status"] == "COMPLETED":
done = True
# really only need resultDocumentId, will address later
return status
elif status["status"] == "FAILED":
print("Failure. Here is the last status:")
print(status)
sys.exit()
else:
print(f"Current status, {status['status']}, percentage: {status['progress']}")
sleep(5)
def downloadResult(doc, path, id, secret):
headers = {
"client_id":id,
"client_secret":secret
}
with open(path, "wb") as output:
bits = requests.get(f"{HOST}/pdf-services/api/documents/{doc}/download", stream=True, headers=headers).content
output.write(bits) input = "../../inputfiles/input.pdf"
print(f"File size of input: {os.path.getsize(input)}")
doc = uploadDoc(input, CLIENT_ID, CLIENT_SECRET)
print(f"Uploaded doc to Foxit, id is {doc['documentId']}")
task = compressPDF(doc["documentId"], "HIGH", CLIENT_ID, CLIENT_SECRET)
print(f"Created task, id is {task['taskId']}")
result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)
print("Done converting to PDF. Now doing linearize.")
task = linearizePDF(result["resultDocumentId"], CLIENT_ID, CLIENT_SECRET)
print(f"Created task, id is {task['taskId']}")
result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)
print("Done with linearize task.")
output = "../../output/really_optimized.pdf"
downloadResult(result["resultDocumentId"], output , CLIENT_ID, CLIENT_SECRET)
print(f"Done and saved to: {output}.")
print(f"File size of output: {os.path.getsize(output)}") This code matches the flow described above, with the exception of outputting the size as a handy way to see the result of the compression call. When run, the initial size is 355994 bytes and the final size is 16733. That's a great saving! You should, however, ensure the result matches the quality you desire and if not, consider reducing the level of compression. Linearize doesn't impact the file size, but as stated above will make it work nicer on the web.
For a complete listing, find the sample on our GitHub repo.
Next Steps
Obviously, you could do even more chaining based on the code above. For example, as part of your optimization flow, you could even split the PDF to return a 'sample' of a document that may be for sale. You could extract information to use for AI purposes and more. Dig more into our PDF Service APIs to get an idea and let us know what you build on our developer forums!
Introducing PDF APIs from Foxit

Get started with Foxit’s new PDF APIs—convert Word to PDF, generate documents, and embed files using simple, scalable REST APIs. Includes sample Python code and walkthrough.
Introducing PDF APIs from Foxit
At the end of June, Foxit introduced a brand-new suite of tools to help developers work with documents. These APIs cover a wide range of features, including:
- Convert between Office document formats and PDF files seamlessly
- Optimize, manipulate, and secure PDFs with advanced APIs
- Generate dynamic documents using Microsoft Word templates
- Extract text and images from PDFs with powerful tools
- Embed PDFs into web pages in a context-aware, controlled manner
- Integrate with eSign APIs for streamlined signature workflows
These APIs are simple to use, and best of all, follow the “don’t surprise me” principal of development. In this post, I’m going to demonstrate one simple example – converting a Word document to PDF – but you can rest assured that nearly all the APIs will follow incredibly similar patterns. I’ll be using Python for my examples here, but will link to a Node.js version of the same example. And given that we’re talking REST APIs here, any language is welcome to join the document party. Let’s dive in.
Credentials
Before we go any further, head over to our developer portal and grab a set of free credentials. This will include a client ID and secret values you’ll need to make use of the API.
Don’t want to read all of this? You can also follow along by video:
API Flow
As I mentioned above, most of the PDF Services APIs will follow a similar flow. This comes down to:
- Upload your input (like a Word document)
- Kick off a job (like converting to PDF)
- Check the job (hey, how ya doin?)
- Download the result
Or, in pretty graphical format –
The great thing is, once you’ve completed one integration (this post focuses on converting Word to PDF), switching to another is easy—and much of your existing code can be reused. A lazy developer is happy developer! Let’s get started.
Loading Credentials
My script begins by loading the credentials and API root host via the environment:
CLIENT_ID = os.environ.get('CLIENT_ID')
CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
HOST = os.environ.get('HOST') It’s never a good idea to hard-code credentials in your code. But if you do it this one time, I won’t tell. Honest.
Uploading Your Input
As I mentioned, in this example we’ll be making use of the Word to PDF API. Our input will be a Word document, which we’ll upload to Foxit using the upload API. This endpoint is fairly simple – aside from your credentials, all you need to provide is the binary data of the input file. Here’s the method I created to make this process easier:
def uploadDoc(path, id, secret):
headers = {
"client_id":id,
"client_secret":secret
}
with open(path, 'rb') as f:
files = {'file': (path, f)}
request = requests.post(f"{HOST}/pdf-services/api/documents/upload", files=files, headers=headers)
return request.json() And here’s how it’s used:
doc = uploadDoc("../../inputfiles/input.docx", CLIENT_ID, CLIENT_SECRET)
print(f"Uploaded doc to Foxit, id is {doc['documentId']}") The upload API only returns one value, a documentId, which we can use in future calls.
Starting the Job
Each API operation is a job creator. By this I mean you call the endpoint and it begins your action. For Word to PDF, the only required input is the document ID from the previous call. We can build a nice little wrapper function like so:
def convertToPDF(doc, id, secret):
headers = {
"client_id":id,
"client_secret":secret,
"Content-Type":"application/json"
}
body = {
"documentId":doc
}
request = requests.post(f"{HOST}/pdf-services/api/documents/create/pdf-from-word", json=body, headers=headers)
return request.json() And then call it like so:
task = convertToPDF(doc["documentId"], CLIENT_ID, CLIENT_SECRET)
print(f"Created task, id is {task['taskId']}") The result of this call, if no errors were found, isa taskId. We can use this to gauge how the job’s performing. Let’s do that now.
Job Checking
Ok, so the next part can be a bit tricky depending on your language of choice. We need to use the task status endpoint to determine how the job is performing. How often we do this, how quickly and so forth, will depend on your platform and needs. For our little sample script here, everything is running at once. I wrote a function that will check the status. If the job isn’t finished (whether successful or not), it pauses briefly before trying again. While this approach isn’t the most sophisticated, it should work well enough for basic testing:
def checkTask(task, id, secret):
headers = {
"client_id":id,
"client_secret":secret,
"Content-Type":"application/json"
}
done = False
while done is False:
request = requests.get(f"{HOST}/pdf-services/api/tasks/{task}", headers=headers)
status = request.json()
if status["status"] == "COMPLETED":
done = True
# really only need resultDocumentId, will address later
return status
elif status["status"] == "FAILED":
print("Failure. Here is the last status:")
print(status)
sys.exit()
else:
print(f"Current status, {status['status']}, percentage: {status['progress']}")
sleep(5) As you can see, I’m using a while loop that—at least in theory—will continue running until a success or failure response is returned, with a five-second pause between each call. You can adjust that interval as needed—test different values to see what works best for your use case. Typically, most API calls should complete in under ten seconds, so a five-second delay felt like a reasonable default.
Each call to the endpoint returns a task status result. Here’s an example:
{
'taskId': '685abc95a0d113558e4204d7',
'status': 'COMPLETED',
'progress': 100,
'resultDocumentId': '685abc952475582770d6917b'
} The important part here is the status. But you could also use progress to give some feedback to the code waiting for results. Here’s my code calling this:
result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)
print(f"Final result: {result}") Downloading Your Result
The last piece of the puzzle is simply saving the result. If you noticed above, the task returned a resultDocumentId value. Taking that, and the [Download Document](NEED LINK) endpoint, we can build a utility to store the result like so:
def downloadResult(doc, path, id, secret):
headers = {
"client_id":id,
"client_secret":secret
}
with open(path, "wb") as output:
bits = requests.get(f"{HOST}/pdf-services/api/documents/{doc}/download", stream=True, headers=headers).content
output.write(bits) And finally, call it:
downloadResult(result["resultDocumentId"], "../../output/input.pdf", CLIENT_ID, CLIENT_SECRET)
print("Done and saved to: ../../output/input.pdf") And that’s it! While this script could certainly benefit from more robust error handling, it demonstrates the basic flow. As mentioned, most of our APIs follow this same logic.
Next Steps
Want the complete scripts? Get it on GitHub.
Want it in Node.js? Get it on GitHub.
Rather try this yourself? Sign up for a free developer account now. Need help? Head over to our developer forums and post your questions and comments.