eSignature API: A Developer’s Guide to Adding Signing to Your App

Illustration of code calling an esignature API alongside a Sign Document screen with a signature field

Adding a signing step to your app involves more than it first appears. Authentication, document preparation, session handling, and completion tracking all need to work together. This guide walks through a full esignature API integration with Foxit eSign, from your first authenticated request to a signed, webhook-confirmed document.

Adding a signing step to an existing app sounds straightforward until you try to implement it. Getting a document signed is a simple idea, but the actual API surface, how authentication works, how you mark up a PDF, and how you learn that signing finished all take longer to figure out than they should. This guide walks a complete Foxit eSign API integration from the first authenticated request through to a webhook-confirmed, digitally signed document. Comfort with REST APIs and bearer tokens is enough to follow along.

What an eSignature API is and how signing workflows work

An eSignature API is a REST interface that owns the document-signing lifecycle, covering preparation, delivery, the signing session, and the audit trail. You supply the document and the signers, and the API handles field rendering, identity capture, signature application, and tamper-evident recordkeeping, so none of that infrastructure is yours to build.

The distinction that matters most for app developers is redirect-based versus embedded signing. Redirect-based signing sends the user to a hosted URL to sign and returns them afterwards, which means they leave your product mid-task. Embedded signing renders the session inside your own application, typically in an iframe, so the user never changes context. If signing sits in the middle of an onboarding or checkout flow, embedded keeps that flow intact.

Foxit eSign supports both. The lifecycle you implement runs through five stages, starting with OAuth 2.0 authentication, then PDF preparation with Text Tags, a POST to /esign/api/v1/folders/createfolder that defines signers and mints a session, the signer completing the embedded session, and a folder_executed webhook confirming the document is final.

Diagram of the esignature API signing lifecycle from OAuth authentication through webhook confirmation

The five stages this guide implements in order. Each one maps to a section below.

Prerequisites

Signing up for eSign API access is self-serve on the Foxit API Platform — no sales ticket and no waiting for an administrator. The platform provisions a trial eSign account for you and manages its credentials, so the whole setup is a short walkthrough:

  1. Create and sign in to your Foxit API Platform account. You need an active Foxit API plan and a complete CAS profile — first name, last name, email address, and company name (a company address is required for eSign provisioning). Your eSign account is provisioned from those profile details, so complete them before you start.
  2. Open the Dashboard and select Get started with eSign in the Dashboard header. That takes you to the eSign activation page.
  3. Choose your document-storage region. The activation card preselects United States, with European Union and Canada as alternatives. Select Activate with [region] storage. Confirm the region before activating: once the remote account exists, the region is locked, and changing it later means submitting a case with Foxit Support.
  4. Wait for provisioning to complete. You may see “Creating your eSign account” and then “Confirming your eSign account” — the second is a reliability check that reconciles ambiguous creation results, so let it finish instead of retrying activation.
  5. Confirm eSign is ready. A successful activation displays “eSign is ready”, your eSign company number, and the selected region. The platform manages the API account and its credentials, and the same unified client_id/client_secret you use for PDF Services also authenticate eSign API, Document Generation, and Embed API — there is no separate eSign key pair to generate. If credential retrieval is still pending or failed, use Resume credentials or Retry credentials on the page; the first-call button stays disabled until credentials are ready.
  6. (Optional but worth it) Run the sample request on the activation page. It uses your profile’s name and email as the first signing party, sends a Base64-encoded one-page contract, adds Signer Name, Today’s Date, Signature, and Date Signed fields, creates a draft with sending disabled, and returns an embedded sending session URL — a quick end-to-end check that everything is wired up. The button switches to Running and then reports “Sample draft created. The embedded sending session is ready.”

Two things to know before you build: your provisioned account is an eSign Business trial that lasts 30 days and starts in TEST mode, so every envelope carries a watermark. Moving the account to Production mode is a Sales/Operations step performed in Foxit Monitor, not something the API can do.

Beyond the account, the rest of the prerequisites are lightweight:

  • Python 3.8+ with pip and a venv, for the webhook handler later in this guide.
  • Flask and requests for the sample code.
  • curl for the token exchange, and ngrok or any tunnel that gives your local webhook endpoint a public HTTPS URL.
  • A code editor, VS Code with the Python extension being a reasonable default alongside PyCharm.
  • A tagged sample PDF, so you do not have to author one. This guide uses agreement-signable.pdf, which already carries Text Tags for a single signer.

Scaffold the workspace in one shot:

mkdir foxit-esign && cd foxit-esign
python3 -m venv .venv && source .venv/bin/activate
pip install flask requests
export ESIGN_HOST="https://na1.foxitesign.foxit.com"export ESIGN_CLIENT_ID="your_api_key"
export ESIGN_CLIENT_SECRET="your_api_secret"
export WEBHOOK_SECRET="your_webhook_secret"

Step 1: Authenticate with the Foxit eSign API

The platform provisions your eSign account and manages its credentials, and that single pair is shared across eSign API, PDF Services, Document Generation, and Embed API. No bearer token, no client_credentials grant, no expires_in to watch.

The quickest way to prove a credential pair works is a minimal folder creation — the same /esign/api/v1/folders/createfolder endpoint Step 3 explains field by field:

curl -X POST "$ESIGN_HOST/esign/api/v1/folders/createfolder" \
  -H "client_id: $ESIGN_CLIENT_ID" \
  -H "client_secret: $ESIGN_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "folderName": "Auth check",
    "inputType": "url",
    "fileUrls": ["https://github.com/lucienchemaly/foxit-demo-templates/raw/main/esign/agreement-signable.pdf"],
    "fileNames": ["agreement.pdf"],
    "parties": [
      {
        "firstName": "Jane",
        "lastName": "Smith",
        "emailId": "[email protected]",
        "permission": "FILL_FIELDS_AND_SIGN",
        "sequence": 1
      }
    ],
    "processTextTags": false,
    "processAcroFields": false,
    "createEmbeddedSigningSession": false,
    "createEmbeddedSendingSession": true,
    "sendNow": false
  }'

A valid credential pair returns JSON with a folder object carrying folderId and folderStatus (DRAFT here, since sendNow is false), so the response doubles as your auth check — a rejected pair comes back as an error before any folder exists. Send the same two headers on every subsequent request. There is nothing to mint or refresh like an OAuth token; if the platform ever flags the stored credentials as stale, refresh them from the activation page with Resume credentials or Retry credentials. And because eSign shares the PDF Services credential pair, the two integrations are interchangeable — one pair of credentials covers both.

Step 2: Prepare a document and define signature fields with Text Tags

Foxit eSign reads field definitions out of the PDF itself at upload time. You embed them as Text Tags, plain-text strings placed where each field belongs, and the API converts them into interactive fields on ingest.

The syntax is ${fieldtype:party_number:required:field_name:width}, where y marks a field required and n marks it optional, the party number maps to a signer’s sequence, and width is expressed as underscores.

${signfield:1:y:____}                 # required signature, party 1
${datefield:1:y::____}                # required date, party 1
${i:1:______}                         # initials field, party 1
${t:1:y:Full_Name:__________}         # required text field, party 1, named "Full_Name"
${signfield:2:y:____}                 # required signature, party 2

Each tag names a type, either in full or by its short alias. The supported set covers signfield (s), initialfield (i), datefield (d), textfield (t), textboxfield (tb), checkboxfield (c), radiobuttonfield (rb), securedfield (sc), attachmentfield (a), imagefield (img), accept (ab), decline (db), payfield (pf), and formulafield (ff). Author them in lowercase to match the documented syntax.

Express width as underscores and never as a literal space, because a space stops the tag from being recognized. A tag written ${s:1: } renders in the signing UI as plain ${s:1: } text with no field attached, while ${signfield:1:y:____} becomes a real signature field. That failure is silent, so the create call still succeeds and you only notice when a signer has nothing to sign.

Two preparation details save support tickets later. Foxit eSign converts tags to fields but does not delete the tag text, so set the tag’s text color to match the page background if you do not want signers reading raw ${...} strings. And paste tags through a plain-text editor first, because smart-quote autocorrect in Word or Google Docs silently swaps straight ASCII characters for typographic ones and tag parsing then fails without an error.

If you would rather not tag a document by hand for a first run, agreement-signable.pdf is already prepared and hosted at a public URL you can pass straight to the next step.

Step 3: Send the document and mint an embedded session

One call to /esign/api/v1/folders/createfolder submits the document, defines the signers, and, when you ask for it, returns a ready-to-render signing URL. Foxit calls the signing container a folder rather than an envelope.

{
  "folderName": "Customer Agreement - Acme Corp",
  "fileUrls": ["https://github.com/lucienchemaly/foxit-demo-templates/raw/main/esign/agreement-signable.pdf"],
  "fileNames": ["agreement.pdf"],
  "parties": [
    {
      "firstName": "Jane",
      "lastName": "Smith",
      "emailId": "[email protected]",
      "permission": "FILL_FIELDS_AND_SIGN",
      "sequence": 1
    }
  ],
  "processTextTags": true,
  "createEmbeddedSigningSession": true,
  "embeddedSignersEmailIds": ["[email protected]"],
  "sendNow": false
}

In this body you point the API at the tagged PDF with fileUrls and give it a display name through fileNames, define the single signer in parties with firstName, lastName, emailId, the FILL_FIELDS_AND_SIGN permission and a signing sequence, then set processTextTags to true so the embedded tags become real fields. Asking for createEmbeddedSigningSession and naming the signer in embeddedSignersEmailIds returns a session URL in the same response, and sendNow set to false keeps Foxit from emailing an invitation, which is what you want while testing.

One nuance to expect here. sendNow: false on its own produces a DRAFT folder, but pairing it with createEmbeddedSigningSession returns folderStatus of SHARED, since the folder has to be live for the session URL to open. No email goes out either way, so the flag still suppresses the invitation, and the status you see just reflects that the document is now signable. To attach the file as bytes instead of a URL, send base64FileString as an array together with inputType set to "base64".

The response nests the folder identifier at folder.folderId and carries an embeddedSigningSessions array. Each entry holds emailIdOfSigner, the raw embeddedToken, and the renderable embeddedSessionURL, which follows this shape:

https://{HOST_NAME}/embedded/embeddedsign?eetid={URL-ENCODED-EMBEDDED-TOKEN}

Three behaviors are worth designing around before you ship. Omitting embeddedSignersEmailIds returns email id of embedded signer(s) not submitted, so always list embedded signers explicitly. Every party number used in a Text Tag needs a matching entry in parties, because a sendNow: true create still reports success while silently dropping the fields of a party that is not listed, which means a mandatory signature is never routed. And for a multi-party document where everyone signs in your app, createEmbeddedSigningSessionForAllParties set to true covers all recipients rather than naming them one by one.

To dispatch a draft later, POST to /api/folders/sendDraftFolder.

Folder state moves through DRAFT, SHARED, COMPLETED, and finally EXECUTED once the digital signature is applied. A run of this guide’s single-signer flow produced exactly that path, with the activity log labelling the creation event CREATED and then recording Envelope viewed, Jane Smith signed this folder at COMPLETED, and Document(s) successfully executed at EXECUTED. PARTIALLY SIGNED appears only when a folder has more than one party and some but not all of them have signed, so you will not see it on a single-signer document.

Step 4: Render the signing session in your app

Load the embeddedSessionURL in an iframe. The sandbox attribute needs a specific minimum set of permissions, and trimming it is a common way to break the signing UI with no visible error.

<iframe
  id="signing-session"
  src="PASTE_EMBEDDED_SESSION_URL_HERE"
  width="100%"
  height="780px"
  style="border: none;"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation"
></iframe>

In this markup the src receives the embeddedSessionURL from the createfolder response, and the five sandbox permissions are the minimum the signing UI needs. Removing allow-popups or allow-top-navigation breaks the flow in ways that surface no obvious error, so keep all five unless you have tested a reduced set end to end. Session URLs are short-lived, so generate one when the user is ready to sign rather than caching it, and request a fresh one per signer through /esign/api/v1/embedded/regenerateEmbeddedSigningSession when a session goes stale.

To check the flow before wiring it into your own UI, download the ready-to-run iFrame test page, open it in a browser, paste your embeddedSessionURL into the input, and load it. A correctly tagged document renders with active signing controls.

Embedded esignature API signing session showing four required fields still outlined and unfilled

A real session opened from the embeddedSessionURL this guide’s request returns. All four tags in the sample became required fields, which is what the counter is reporting. The raw tag text still shows through each box, which is exactly why you color it to match the page background before shipping.

Step 5: Confirm completion with webhooks

Polling for completion wastes requests and adds latency. Register a webhook instead and Foxit eSign posts to your endpoint as each signing event happens.

Registration lives on the eSign portal’s API settings page at /consumer/consumerdetails, under Configure Webhooks, where you set the callback URL, a webhook secret, and the events you want. That page is visible only to the account owner, so an admin-level user will not find it. Your endpoint has to be reachable over public HTTPS.

Foxit eSign webhook configuration screen with callback URL, webhook secret, and signing event checkboxes

The owner-only webhook settings. The event checkboxes control which callbacks reach your endpoint.

The events available are folder_sent, folder_viewed, folder_signed, folder_cancelled, folder_executed, folder_deleted, folder_completed, folder_assigned, and folder_access_code_failure. In practice folder_viewed, folder_signed, folder_completed, and folder_executed are the ones that fire on an API-dispatched folder.

The event to build on is folder_executed. folder_completed fires once every party has signed, but folder_executed fires after Foxit applies the digital signature and locks the audit trail, so it is the point at which a download gives you the final document.

Foxit signs every callback. It delivers the POST as <your-url>?signature=<base64>, where the signature is the base64 of an HMAC-SHA-256 over the raw request body keyed with your webhook secret. Verify it against the unparsed bytes, since re-serializing the JSON changes whitespace or key order and breaks the comparison.

import base64
import hashlib
import hmac
import os

from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]


def verify_signature(raw_body: bytes, signature: str) -> bool:
    expected = base64.b64encode(
        hmac.new(WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, signature)


@app.route("/webhooks/esign", methods=["POST"])
def handle_esign_event():
    raw_body = request.get_data()
    if not verify_signature(raw_body, request.args.get("signature", "")):
        return jsonify({"error": "invalid signature"}), 403

    payload = request.get_json(silent=True) or {}
    event_name = payload.get("event_name")
    folder = payload.get("data", {}).get("folder", {})

    if event_name == "folder_executed":
        # The document is final here. Archive it, update your record, notify the user.
        app.logger.info("Signing finished for folder %s", folder.get("folderId"))

    return jsonify({"status": "received"}), 200

In this handler you read the unparsed body first, recompute the HMAC over those exact bytes with your webhook secret, and compare it against the signature query parameter using hmac.compare_digest so the check is not timing-dependent. A mismatch returns 403 before any business logic runs, which stops a spoofed POST to your public URL from triggging an archive. Only after the signature passes do you parse the JSON, read event_name, and branch on folder_executed to run downstream work, returning 200 so Foxit records the delivery as successful. A ready-to-run version of this receiver lives at webhook_receiver.py in the demo repo.

Test the whole path against a draft. Create the folder with sendNow set to false, dispatch it, sign in the embedded session, and watch the events arrive. Once the signer finishes, the session redirects to a result URL carrying event=signing_success, and the document reaches EXECUTED within a few seconds.

Completed embedded signing session with name, initials, date, and signature filled in and Finish enabled

The session once every field is filled. The counter reads zero and Finish activates, which is the state that produces folder_completed and then folder_executed.

Step 6: Retrieve the executed document and its audit trail

With folder_executed in hand, pull the final PDF. The download takes the folder id as a query parameter.

curl -o executed.pdf \
  "$ESIGN_HOST/api/folders/download?folderId=35117696" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The response streams the executed PDF, which arrives with a content type of application/octet-stream rather than application/pdf, so write the bytes to a file rather than sniffing the header. The returned document carries the filled field values and the signature certificate, so a text extraction of it contains the signer’s typed name, the date, and the per-signer Signer ID recorded on the certificate page.

For the audit trail itself, GET /api/folders/viewActivityHistory?folderId={id} returns a details object holding the folder metadata plus an activities array, where each entry carries an activity description and the folderStatus at that moment. That endpoint is GET-only and only returns data once the folder has been shared, so a pure DRAFT folder reports nothing useful.

The signature certificate attached to a document once folder_executed has fired. Each signer’s adopted signature sits beside the identity record Foxit captured, which is the tamper-evident audit trail you are archiving alongside the PDF.

Common mistakes

  • Dropping the /api/ prefix : every eSign path sits under /api/, so it is /api/folders/createfolder, not /folders/createfolder.
  • snake_case request fields : the body is camelCase. folderName, fileUrls, emailId, and sendNow work, while folder_name, document_url, email, and send_now do not.
  • Omitting processTextTags : without it the tags stay inert text, the signer sees raw ${...} strings, and they can finish without signing anything.
  • A tag party with no matching parties entry : the create returns success and that party’s fields disappear silently, so their signature is never collected.
  • Skipping signature verification : a public webhook URL that acts on any POST is a spoofing target. Verify the HMAC before you touch the payload.
  • Archiving on folder_completed : that fires before the digital signature is applied. Wait for folder_executed.
  • Caching an embeddedSessionURL : sessions expire. Mint one when the signer is ready, and regenerate when needed.
  • Trimming the iframe sandbox list : removing allow-popups or allow-top-navigation breaks signing with no error message.
  • Smart quotes or a space inside a tag : both stop tag recognition. Use straight ASCII and underscores.
  • Reusing PDF Services credentials : eSign has its own portal, host, and key pair.

eSignature API FAQ

A REST interface that handles the document-signing lifecycle, covering preparation, delivery, the signing session, and the audit trail, so your app makes calls rather than building signing infrastructure.

Embedded signing renders the session inside your own application instead of redirecting to a hosted page, so the signer finishes without leaving your product. That matters most when signing sits inside a flow you do not want to interrupt, like onboarding or checkout.

The party number in a tag maps to a signer’s sequence in the parties array, so ${s:1: } is signed by the party with sequence: 1. Keeping those aligned is the difference between fields routing correctly and vanishing silently.

folder_completed fires when all parties have signed. folder_executed fires after Foxit applies the digital signature and locks the audit trail, which is why downstream archiving should trigger on folder_executed.

Recompute a base64 HMAC-SHA-256 of the raw request body using your webhook secret and compare it with the signature query parameter. Verify against raw bytes, never re-serialized JSON.

Yes. Pass additional entries in fileUrls with matching fileNames. Each document carries its own Text Tags, and party assignments stay consistent across the folder.

No. The free tier gives you credentials and lets you run the full flow, and no credit card is required to create the account.

Yes. Create it with sendNow set to false for a DRAFT folder, inspect it in the eSign dashboard, then dispatch with /api/folders/sendDraftFolder.

Wrapping up

That is the full path for an eSignature API integration. Authenticate with the client_credentials grant, prepare a PDF with Text Tags, create the folder with processTextTags and an embedded session, render the returned embeddedSessionURL in an iframe, and act on a signature-verified folder_executed webhook.

The same shape extends to multi-signer approval chains, CRM-triggered signing when a deal closes, and template-based documents where fields arrive pre-filled from your own data. Those build on the pieces already in place rather than replacing them.

Ready to build? Create a free account (no credit card required) at account.foxit.com/site/sign-up, generate your API Key and Secret from the API tab, and run the token call above against agreement-signable.pdf to see a session URL come back.

Build a CRM-Triggered PDF Generation and eSign Workflow in Power Automate with Foxit’s REST APIs

Diagram of an api workflow automation pipeline connecting a CRM trigger, Foxit document generation, eSign, and cloud archive

This guide shows how to trigger a Word-to-PDF contract from a closed CRM deal, route it for signature through Foxit’s eSign API, and archive the signed copy automatically, using nothing but HTTP actions and a webhook.

Most Power Automate document tutorials stop at a SharePoint file move or an AI Builder extraction, and the ones that touch signatures assume a native connector that does not exist for a headless, API-first pipeline. The gap is the full chain, where a CRM deal closes, a contract is generated from a Word template, the PDF is routed for signature, and the signed copy is archived, with no manual export and no polling. This article builds that pipeline in Power Automate using the HTTP action to call Foxit’s REST endpoints directly, which is the same pattern that carries over to n8n, Zapier, or any orchestrator that can make an HTTP request.

Foxit exposes two REST APIs that chain cleanly for this. The Document Generation API takes a base64-encoded Word template plus a JSON payload and returns a base64 PDF, and the eSign API handles the signing lifecycle behind an OAuth2 token. The base64 PDF from generation drops straight into the eSign upload call, so the handoff stays inside one flow with no SDK to install and no desktop agent. This Power Automate Foxit API integration walks the two hosts and their two auth models, the document generation call, the send-for-signature call with embedded signature fields, and a second flow that receives Foxit’s webhook and archives the executed document.

Prerequisites

Power Automate runs in the browser, so there is no local runtime to install. What you need are the right accounts and one sample file.

  • A Power Automate account with the HTTP action : the HTTP action is a premium connector, so you need a per-user or per-flow premium license. This is the one paid dependency in the tutorial. On the free Microsoft 365 tier you will hit a wall at the first HTTP action, so confirm the plan before you start.

  • A Foxit Developer account : create one at account.foxit.com/site/sign-up and activate the free Developer plan, which includes 500 credits per year with no credit card. From the APIs Dashboard, copy the Document Generation Client ID and Secret, and separately the eSign Client ID and Secret. These are two different credential pairs for two different APIs.

  • A CRM that can trigger a flow : this article uses the Salesforce connector as the example. Any CRM with a Power Automate connector, or any system that can POST to a webhook URL, works the same way.

  • The sample contract template : download contract_signing.docx so you do not have to author one. It already carries both Document Generation merge tags and eSign signature tags, which is what makes the two-API handoff work.

  • A REST client : Postman or curl, to test each Foxit call in isolation before wiring it into a flow.

Store both Foxit credential pairs in the Power Automate secure store or as environment variables in your solution, never pasted as literals into an action, so they do not travel in exported flow definitions.

Foxit APIs Dashboard displaying the base URL, Client ID, and Client Secret for an API application

The APIs Dashboard is where you retrieve the Client ID and Secret. Document Generation and eSign each have their own pair.

How the Power Automate Foxit API Integration Works: Two APIs, Two Hosts, Two Flows

The pipeline has four stages. A CRM Closed Won event triggers document generation, the generated PDF is sent for signature, and the executed document is archived once every party has signed.

Salesforce: Opportunity → Closed Won
        │
        ▼
HTTP: POST GenerateDocumentBase64   (na1.fusion.foxit.com)   → base64 PDF
        │
        ▼
HTTP: POST createfolder             (na1.foxitesign.foxit.com) → folderId, email sent
        │
   … signer signs (async) …
        ▼
Flow 2: When a HTTP request is received  <- Foxit webhook (folder_executed)
        │
        ▼
HTTP: GET download → OneDrive: Create file

Power Automate owns orchestration and control flow. Foxit owns document rendering and the signature lifecycle. The single most common mistake in this integration is treating the two Foxit products as one, so keep them separate from the start. Document Generation runs on https://na1.fusion.foxit.com and authenticates with lowercase client_id and client_secret request headers. eSign runs on https://na1.foxitesign.foxit.com and authenticates with an OAuth 2.0 bearer token obtained from its own credential pair. They do not share credentials or a portal.

The work also splits into two flows for a reason. Generation and sending are synchronous, so they belong in one flow that runs when the deal closes. Signing completes minutes or days later, so a second flow triggered by Foxit’s webhook handles the archive step. That separation is what removes any polling from the design.

Here is the main flow built in the Power Automate designer, with the four HTTP actions chained after the trigger.

Power Automate designer showing the api workflow automation sequence from trigger through Create signing folder

The four HTTP actions in order. This build uses a manual trigger so the flow runs on demand while you test; in production the Salesforce trigger from Step 1 takes its place as the entry point, and nothing downstream changes.

Step 1: CRM Trigger, Firing the Flow on a Closed Deal

Create an automated cloud flow and choose the Salesforce trigger for a created or modified record, pointing it at the Opportunity object. Add a condition so the flow only proceeds when the Stage equals Closed Won, which keeps every mid-pipeline edit from generating a contract.

Map the CRM fields the contract needs. For the sample template, that is the client name, the contract date, the deal value, and the signer’s name and email. Read them from the trigger output with expressions like triggerOutputs()?['body/Account_Name'] and store each in a variable or reference it inline, so the next two steps can assemble their payloads cleanly.

This step is swappable. Any CRM with a Power Automate connector, or any system that can POST JSON to a Power Automate HTTP-request trigger, drops in here without touching the Foxit calls that follow. If you use HubSpot or Dynamics 365, only the trigger and field paths change, so the rest of this tutorial stays identical.

Step 2: Generate the Contract PDF with GenerateDocumentBase64

The contract_signing.docx template drives this step. It contains Document Generation merge tags for the scalar fields and a table loop for line items, using Foxit’s {{ }} syntax:

{{clientName}}
{{contractDate \@ MM/dd/yyyy}}
{{dealValue \# "$#,##0.00"}}

{{TableStart:lineItems}} {{description}}   {{amount}} {{TableEnd:lineItems}}

The \@ switch formats a date and the \# switch formats a currency value, so you can pass a raw ISO date and a plain number and let the template render them. The {{TableStart:lineItems}} and {{TableEnd:lineItems}} tokens sit in a single Word table row and repeat that row for each object in the lineItems array.

Before the flow can send the template, it needs the file as a base64 string. The build here gets it with an HTTP GET action named Get template, pointed at the raw contract_signing.docx URL, and base64-encodes the response with the base64() expression. For a template you maintain yourself, store it in OneDrive or SharePoint and use a Get file content action instead of the GET. Then add a second HTTP action named Generate PDF, set to POST https://na1.fusion.foxit.com/document-generation/api/GenerateDocumentBase64 with client_id and client_secret headers and this JSON body:

{
  "base64FileString": "@{base64(body('Get_template'))}",
  "documentValues": {
    "clientName": "@{variables('clientName')}",
    "contractDate": "2026-07-17",
    "dealValue": 48500,
    "lineItems": [
      { "description": "Platform license (annual)", "amount": "$36,000.00" },
      { "description": "Onboarding and training", "amount": "$12,500.00" }
    ]
  },
  "outputFormat": "pdf"
}

In this code, you send the base64-encoded template as base64FileString, pass the CRM-sourced fields as documentValues whose keys match the template tags exactly, and request a PDF with outputFormat set to the lowercase "pdf". The keys clientName, dealValue, and the lineItems array with its description and amount fields have to match the tag names in the Word file, since a mismatch leaves the tag unrendered rather than raising an error. The call is synchronous and returns HTTP 200 with a JSON response containing message, fileExtension, and base64FileString, where base64FileString is the rendered PDF as a base64 string.

The call is synchronous, so there is no task to poll and no status to check, and the generated contract is available immediately. Reference the rendered PDF in the next step directly with the expression body('Generate_PDF')?['base64FileString'], or add a Parse JSON action first if you prefer typed outputs. When you need the raw bytes rather than the string, for archiving or a file action, convert with the base64ToBinary() expression, one of the Workflow Definition Language conversion functions.

One limit to plan for is the upload size. Document Generation rejects .docx payloads larger than 4 MB after base64 encoding, which is roughly a 3 MB raw file, and the limit is not surfaced in a friendly error. If you hit it, compress images through Word’s Picture Format tools, drop embedded fonts and OLE objects, and split oversized templates. The sample here is about 37 KB, so first runs stay well clear of the cap. For template authoring detail and common payload errors, Foxit’s Document Generation API quickstart is the reference.

Generated Service Agreement PDF with client details, line-item table, and embedded signature fields

The generated contract. The signature and date fields at the bottom come from the eSign Text Tags embedded in the same template, ready for Step 3.

Step 3: Send for Signature with OAuth2 and createfolder

The eSign API needs a bearer token first. Add an HTTP action that POSTs to https://na1.foxitesign.foxit.com/api/oauth2/access_token with content type application/x-www-form-urlencoded and this body:

grant_type=client_credentials&client_id=YOUR_ESIGN_CLIENT_ID&client_secret=YOUR_ESIGN_CLIENT_SECRET&scope=read-write

Name this action Get eSign token. Its JSON response carries access_token, token_type set to bearer, expires_in, and instance_url, and you reference the token in the next call as body('Get_eSign_token')?['access_token']. This is the client-credentials grant, which fits a server-to-server flow where no human is present to log in.

The signature fields are already defined in the template. contract_signing.docx carries two eSign Text Tags, ${signfield:1:y} and ${datefield:1:y}, which follow the ${fieldtype:party:mandatory} syntax. Here both are mandatory fields assigned to party 1, the client who signs. Because the tags live in the Word file, the PDF that Document Generation produced in Step 2 already has a signature and date field in place, so there is no manual field placement after generation. Keep two rules in mind when you author your own tags. Replace any space inside a tag with an underscore, since a literal space breaks tag recognition, and set the tag text color to match the document background so the tokens do not show in the final PDF.

Now add the send action, an HTTP action named Create signing folder, set to POST https://na1.foxitesign.foxit.com/api/folders/createfolder with an Authorization: Bearer @{body('Get_eSign_token')?['access_token']} header and this body:

{
  "folderName": "Acme Corp Contract",
  "inputType": "base64",
  "base64FileString": ["@{body('Generate_PDF')?['base64FileString']}"],
  "fileNames": ["contract_signing.pdf"],
  "parties": [
    {
      "firstName": "Jordan",
      "lastName": "Lee",
      "emailId": "[email protected]",
      "permission": "FILL_FIELDS_AND_SIGN",
      "sequence": 1
    }
  ],
  "processTextTags": true,
  "sendNow": true
}

In this code, you attach the Step 2 PDF by passing it as the single element of the base64FileString array together with inputType set to "base64", which is the pairing the API requires for base64 uploads. The parties array names the signer with firstName, lastName, emailId, and a permission of FILL_FIELDS_AND_SIGN, and its sequence of 1 matches the party number in the ${signfield:1:y} tag. Setting processTextTags to true is what converts those embedded tags into real, interactive fields, and sendNow set to true dispatches the signing invitation immediately. The response returns result as success and nests the identifier at folder.folderId, which you store for the archive flow. Foxit calls this container a folder rather than an envelope.

Create signing folder HTTP action configured with the eSign bearer token and base64 PDF body

The Create signing folder action. The access_token and base64FileString chips are dynamic references to the two prior HTTP actions, so the eSign token and the generated PDF flow straight into this call with no copy-paste.

Two behaviors are worth handling before you ship. Every party number referenced by a Text Tag must have a matching entry in the parties array, because if a tag points at a party that is not listed, a sendNow: true create still returns success but silently drops that party’s fields, so their signature is never routed. If you need a human to approve the contract before it leaves, set sendNow to false to create a draft without emailing anyone, then dispatch it later with POST https://na1.foxitesign.foxit.com/api/folders/sendDraftFolder.

Foxit eSign signing view showing an active click-to-sign field on the generated contract

What the recipient sees after createfolder sends the invitation. The signature field is the one defined by the ${signfield:1:y} tag.

Step 4: Receive the Webhook and Archive the Signed Document

Create a second automated flow using the When a HTTP request is received trigger. Saving the flow generates a callback URL. Register that URL as the webhook endpoint on the eSign portal’s API settings page, which is owner-only, and select the signing events you want delivered.

Archive on the right event. The folder status moves through DRAFT, then SHARED, then PARTIALLY SIGNED, then COMPLETED, then EXECUTED. Trigger the archive on folder_executed, not folder_completed. The folder_completed event fires when all signatures are in but before the digital signature has been applied to the PDF, whereas folder_executed guarantees the file you download is the final, digitally signed document.

Verify each callback before acting on it. Foxit delivers every webhook as POST <your-url>?signature=<base64>, where the signature is the base64 of an HMAC-SHA-256 of the raw request body keyed with your webhook secret. Power Automate’s expression language has no native HMAC function, so there are two practical paths. The stronger one calls a small Azure Function or an Office Script that recomputes the HMAC over the raw body and returns a match boolean, which the flow checks in a Condition. The lighter one restricts the trigger to your tenant and treats the webhook secret as a shared value checked in a Condition, which is simpler but weaker. Choose based on how exposed the endpoint is.

Once a callback is verified and the event is folder_executed, parse folderId from the payload and download the signed file. Add an HTTP action with the bearer token set to GET https://na1.foxitesign.foxit.com/api/folders/download?folderId=@{triggerBody()?['folderId']}, which returns the executed PDF as application/pdf. Pass the response body to the OneDrive for Business Create file action, naming the destination folder by deal name or date so contracts stay retrievable.

The owner-only webhook settings, where you paste the Power Automate callback URL and pick the events to receive.

If you cannot expose a public endpoint, poll instead. GET https://na1.foxitesign.foxit.com/api/folders/viewActivityHistory?folderId={id} returns the audit trail, with actions such as Created, Invitation Sent, Opened, Viewed, Signed, and Folder Executed. This endpoint is GET-only and only returns data once the folder has been shared or sent, so run it on a schedule from a separate flow.

Common Mistakes

Most failures in this pipeline come from a small set of recurring errors. Check these first when something does not work.

  • Using one credential pair or one token for both APIs : Document Generation uses client_id and client_secret headers on na1.fusion.foxit.com, while eSign uses a bearer token on na1.foxitesign.foxit.com. They are separate.
  • Sending the eSign PDF without inputType : the base64 upload needs inputType set to "base64" alongside the base64FileString array, or the API returns fileUrls or base64FileString cannot be empty.
  • Omitting processTextTags : without processTextTags set to true, the tags stay as inert text and the signer can finish without signing.
  • A tag party number with no matching party : if ${signfield:2:y} appears but the parties array has no party 2, the send succeeds and that party’s fields vanish silently.
  • Missing a party email field : each party needs emailId, not email. The wrong key returns email id of party cannot be empty.
  • Archiving on folder_completed : download on folder_executed instead, or you may pull a PDF before the digital signature is applied.
  • A stray space or smart quote in a tag : a literal space inside ${...} or a curly quote from Word autocorrect breaks tag recognition. Use straight quotes and underscores.

API Workflow Automation FAQ

Yes. Any platform that can make HTTP requests and receive a webhook works, since the Foxit calls are identical. Only the orchestration layer changes.

No. The free Developer plan gives 500 credits per year with instant activation and no credit card. The paid dependency here is the Power Automate premium license for the HTTP action.

No. Each API has its own Client ID and Secret, so store two credential pairs and use each on its own host.

Yes. The createfolder call accepts a base64 PDF directly, or a public file URL through fileUrls and fileNames, so the generation step is optional if you already have the document.

Add party-2 Text Tags to the template, such as ${signfield:2:y}, and a matching party-2 entry to the parties array. Keep the party numbers in the tags and the array aligned.

Next Step

The fastest way to confirm the pieces before building the flow is to run one call by hand. Create your free Foxit Developer account, pull your Document Generation and eSign Client IDs and Secrets from the APIs Dashboard, and run a single GenerateDocumentBase64 call against contract_signing.docx in a REST client. Once it returns a base64 PDF, you know the credentials and payload are right, and the rest of the Power Automate Foxit API integration is just wiring the same calls into actions. Create your free account to get started.

Build a CRM-Triggered Document Pipeline with Foxit APIs in n8n, with a Vercel Workflows Alternative

Diagram of a CRM-triggered document pipeline: contract generation, eSign, and cloud archive

This tutorial shows how to wire a CRM webhook, the Foxit Document Generation API, and the Foxit eSign API into one durable pipeline, without manual copying of data between tools. You’ll build it twice: once visually in n8n, and once as a resumable serverless function in Vercel Workflows.

Most document automation tutorials stop at the happy path. Generate a PDF, send it for signature, done. What they skip is the plumbing, which is where a real pipeline lives or dies. How does a CRM event actually hand off to a document API? How do you pass a signed document downstream without a polling loop? What happens when one step fails halfway through and you do not want to re-run the steps that already succeeded?

If you have tried to stitch a multi-step document workflow across a CRM, a generator, and an eSign service, you know exactly where the seams are. The deal data lives in the CRM, the template lives somewhere else, the signature lives in a third system, and a person ends up copying state between them.

This tutorial closes those seams. A CRM deal-closed event fires a webhook, a workflow renders a contract from a Word template, routes it for signature, waits for completion, and archives the signed file, with no human in the loop. You will build it first in n8n, a source-available workflow tool, and then rebuild the same pipeline as a durable function with Vercel Workflows. Both use the Foxit Document Generation API and the Foxit eSign API with real endpoints and real request shapes.

Durability is the theme. A production pipeline must survive a step failure, retry without repeating completed work, and hold state across an asynchronous signing wait that can last minutes or days.

Architecture Overview: Two Ways to Build the Same Pipeline

The pipeline is four stages, and each hands its output directly to the next.

  1. Trigger. A CRM webhook (a HubSpot deal-stage change in this example) delivers the deal data.

  2. Generate. The workflow renders a contract PDF from a Word template through Document Generation, a synchronous REST call with no SDK to install.

  3. Sign. The rendered PDF goes straight to the eSign API, which creates a signing folder and routes it to the signer.

  4. Archive. When signing completes, the workflow downloads the executed PDF and writes it to storage.

In n8n, that maps to a Webhook node, an HTTP Request node for generation, an HTTP Request node for signing, a second Webhook node that waits for the eSign completion callback, and a final archive step.

n8n canvas for Foxit API pipeline

The core chain in the local n8n editor. Each stage is a plain HTTP Request node, and the base64 PDF from the generation node flows straight into the eSign create-folder node.

The same pipeline can be written as code with Vercel Workflows, where the whole flow is one durable function, each Foxit call is a retrying step, and the eSign wait is a hook that resumes when the completion webhook arrives. Reach for n8n when you want visual orchestration and fast prototyping, and for Vercel Workflows when you want code-native durability and already run on Vercel. Section 7 builds that version.

One detail shapes every call. The two APIs live on two hosts and authenticate differently. Document Generation runs on https://na1.fusion.foxit.com and takes client_id and client_secret request headers. eSign runs on https://na1.foxitesign.foxit.com and takes an OAuth2 bearer token obtained from /api/oauth2/access_token.

Step 1: Credentials and Template Prep

The whole build runs on free infrastructure. Run n8n as the self-hosted Community Edition in Docker, and use Foxit’s free Developer plan. There is no n8n Cloud plan and no paid Foxit tier involved.

Register a Foxit account at account.foxit.com/site/sign-up, verify your email, and activate the free Developer plan. From the APIs Dashboard, copy your Document Generation Client ID and Client Secret. The eSign API is provisioned separately, so once eSign is active on your account, retrieve its API Key and API Secret as a distinct pair. You end up with two credential sets, and they are not interchangeable.

Store the Document Generation pair once in n8n’s credential store rather than pasting keys into nodes. Because Foxit needs two custom headers, use the Custom Auth credential type, which is the same type Foxit’s published template uses. Create a credential with this JSON:

{
  "headers": {
    "client_id": "YOUR_FOXIT_CLIENT_ID",
    "client_secret": "YOUR_FOXIT_CLIENT_SECRET"
  }
}
n8n Custom Auth credential for Foxit API

The Custom Auth credential holds both Foxit headers in one place, so your keys never appear in a node field or an exported workflow.

Now the template. Because the goal is a signing-ready contract, one Word file carries two families of tags. Document Generation merge tags use double braces for data, and eSign Text Tags use dollar-brace syntax for signature fields. Download the ready-to-use sample, contract_signing.docx. It carries {{clientName}}, {{dealValue \# "$#,##0.00"}}, {{contractDate \@ MM/dd/yyyy}}, a {{TableStart:lineItems}} / {{TableEnd:lineItems}} loop for line items, and the eSign Text Tags ${signfield:1:y} and ${datefield:1:y}. The Text Tag format is ${fieldtype:party:mandatory}, so ${signfield:1:y} is a required signature for party 1. Keep the raw file under about 3 MB, for the reason covered in Step 2.

Step 2: CRM Webhook to DocGen, Generating the Contract

Start the workflow with a Webhook node, which is how a CRM will call your pipeline when a deal closes. Set the method to POST and give it a path such as deal-closed. n8n gives the node a Test URL for building and a Production URL for live traffic.

The Webhook node’s Test URL. Click “Listen for test event” to capture a sample request while building.

Because a local build has no real CRM pointed at it, fire the webhook yourself. Click Listen for test event, copy the Test URL, and send a sample deal payload with cURL:

curl -X POST "http://localhost:5678/webhook-test/deal-closed" \
  -H "Content-Type: application/json" \
  -d '{ "clientName": "Acme Robotics", "dealValue": 48500, "contractDate": "2026-07-02" }'

In this request, you POST a small JSON object standing in for the CRM’s deal-closed payload. The Webhook node captures it and its fields become available to later nodes as {{ $json.body.clientName }}. In production, register the node’s Production URL in your CRM’s webhook settings, and expose your local instance with a tunnel such as ngrok if you are testing before deploying.

Add an HTTP Request node for generation. Set the method to POST, the URL to https://na1.fusion.foxit.com/document-generation/api/GenerateDocumentBase64, attach the Custom Auth credential, turn on Send Body, choose JSON, and send:

{
  "base64FileString": "<base64-encoded contract_signing.docx>",
  "documentValues": {
    "clientName": "{{ $json.body.clientName }}",
    "dealValue": "{{ $json.body.dealValue }}",
    "contractDate": "{{ $json.body.contractDate }}",
    "lineItems": [
      { "description": "Platform license (annual)", "amount": "$36,000.00" },
      { "description": "Premium support", "amount": "$5,000.00" }
    ]
  },
  "outputFormat": "pdf"
}

The HTTP Request config panel. Set the method and URL, attach the credential, and turn on Send Body for the JSON payload.

In this body, base64FileString is the template encoded as base64, documentValues is a flat object whose keys match the template tags exactly and whose values come from the webhook payload, and outputFormat is the lowercase "pdf". The keys must match the tag names character for character, because a mismatch does not raise an error. The call returns HTTP 200 and that field simply renders blank, so confirm names with POST /document-generation/api/AnalyzeDocumentBase64 if a field comes back empty. One more limit to plan for, since it surfaces as a bare HTTP 500 rather than a friendly message. Document Generation rejects a .docx payload larger than 4 MB after base64 encoding, which is roughly a 3 MB raw file, so compress images and drop embedded fonts if the template is heavy.

The endpoint is synchronous. The response carries message, fileExtension, and base64FileString, where the last holds the rendered PDF as base64. There is no job id and no polling, which keeps the graph linear. The generated contract has the data merged in and the Text Tags still present in the text, ready for signing.

Generated Foxit contract PDF with signature tags

The generated PDF. The merge tags are filled and the ${signfield:1:y} and ${datefield:1:y} Text Tags survive into the output, where eSign will convert them into fields.

Step 3: Routing the PDF into eSign

The eSign leg starts by turning your eSign credentials into a bearer token. Add an HTTP Request node that POSTs to https://na1.foxitesign.foxit.com/api/oauth2/access_token, with the body sent as Form Urlencoded and four fields, grant_type set to client_credentials, client_id, client_secret, and scope set to read-write. The response is JSON with access_token, token_type (the string bearer), expires_in, and instance_url. This is a standard OAuth2 client-credentials grant defined in RFC 6749. Cache the token against expires_in and refresh before it lapses rather than minting one per run.

The token node uses a Form URL Encoded body with the four OAuth fields. Click Execute step to confirm it returns a token before wiring the rest.

Successful Foxit eSign token response

A successful token call. The green check and the returned access_token confirm the credentials and endpoint are correct.

Now add the send-for-signature node. POST to https://na1.foxitesign.foxit.com/api/folders/createfolder with an Authorization: Bearer <access_token> header and this body:

{
  "folderName": "Contract - {{ $('Webhook').item.json.body.clientName }}",
  "inputType": "base64",
  "base64FileString": ["{{ $('Generate PDF').item.json.base64FileString }}"],
  "fileNames": ["contract.pdf"],
  "processTextTags": true,
  "sendNow": true,
  "parties": [
    {
      "firstName": "Alex",
      "lastName": "Rivera",
      "emailId": "[email protected]",
      "permission": "FILL_FIELDS_AND_SIGN",
      "sequence": 1,
      "workflowSequence": 1
    }
  ]
}
Foxit eSign create-folder request node

The create-folder node. The Authorization header value Bearer {{ $json.access_token }} pulls the token from the previous node.

In this call, inputType is "base64" and base64FileString is an array holding the PDF from Step 2, which is the direct handoff between the two APIs. processTextTags: true converts the embedded Text Tags into real fields, and sendNow: true dispatches the folder in the same request. The response nests the identifier at folder.folderId, which you keep for the completion step. One rule fails silently, so check it. Every party number used in a Text Tag must have a matching entry in parties, or a sendNow: true call returns success while dropping that party’s fields. If you prefer a review gate, set sendNow: false and dispatch later with POST /api/folders/sendDraftFolder.

Foxit eSign signer view with signature field

What the signer receives, from a real run of this exact pipeline. The ${signfield:1:y} tag in the template has become an interactive signature field, which confirms processTextTags did its job on the generated contract.

Step 4: eSign Completion Webhook and Archive

Register your callback URL in the eSign portal’s API settings page, under the Configure Webhooks section. This page is visible to the account owner once API access is active, so a non-owner login will not see it. Provide the HTTPS callback URL, a Webhook Secret, and the events you want delivered.

Archive at the right moment by understanding the lifecycle. A folder moves through DRAFT, then SHARED, then PARTIALLY SIGNED, then COMPLETED when all signatures are collected, then EXECUTED a few seconds later once the digital signature is applied. Hook folder_executed, not folder_completed, so the file you store is the finalized, digitally signed PDF.

Add a second Webhook node to receive the callback. Each callback POSTs a JSON body with event_name, event_date in Unix milliseconds, and data, where data.folder carries folderId, folderStatus, folderDocumentIds, and documentsList. Verify authenticity against the HMAC-SHA-256 digest Foxit computes over the raw request body with your Webhook Secret and appends to the callback URL as a signature query parameter. Compare against the raw body bytes, not a re-serialized payload.

On completion, download the executed document and archive it. A GET to https://na1.foxitesign.foxit.com/api/folders/download?folderId={id} with the bearer token returns the signed PDF as application/pdf. Route that binary to a storage node such as Google Drive, an S3 bucket, or an internal file-system node, then fire any downstream update.

Not every send completes, so handle the abandoned-signer path. If the completion webhook never fires, use an n8n Wait node with a timeout, or poll GET /api/folders/viewActivityHistory?folderId={id} (GET only, and it responds once the folder is shared) to read the audit trail, then branch to a follow-up rather than hanging.

The Vercel Workflows Alternative

If your stack already runs on Vercel, you can build the same pipeline as a durable function instead of a visual graph. Vercel Workflows is built on the open-source Workflow SDK. Scaffold a Next.js app, add the SDK, and wrap the config so the build compiles your workflow functions into durable routes:

npm create next-app@latest foxit-vercel-workflow
cd foxit-vercel-workflow
npm i workflow
// next.config.ts
import { withWorkflow } from "workflow/next";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {};

export default withWorkflow(nextConfig);

A CRM would kick off a run by POSTing to an API route, which starts the workflow with start() and returns immediately while the durable function runs in the background:

// app/api/trigger/route.ts
import { NextResponse } from "next/server";
import { start } from "workflow/api";
import { contractWorkflow, type Deal } from "@/workflows/contract";

export async function POST(req: Request) {
  const deal = (await req.json()) as Deal;
  await start(contractWorkflow, [deal]);
  return NextResponse.json({ started: true });
}

The orchestration function carries the 'use workflow' directive, which makes it resumable and able to survive deploys and crashes through deterministic replay. It generates the contract, sends it for signature, then awaits a hook keyed by the folderId until the signing completes:

// workflows/contract.ts
import { signatureHook } from "./hooks";

export async function contractWorkflow(deal: Deal) {
  "use workflow";

  const pdfBase64 = await generateContract(deal);
  const folderId = await sendForSignature(pdfBase64, deal);

  // Pause here, consuming no compute, until the eSign webhook resumes it.
  const completion = await signatureHook.create({ token: String(folderId) });

  const archivedBytes = await archiveSigned(folderId);
  return { folderId, event: completion.event_name, archivedBytes };
}

Each Foxit call becomes a step. A function with the 'use step' directive runs a unit of durable work and gets built-in retries on transient failures like network errors, so a flaky call is retried without re-running the rest of the pipeline.

// app/steps/generate-contract.ts
async function generateContract(deal: Deal) {
  'use step';

  const res = await fetch(
    'https://na1.fusion.foxit.com/document-generation/api/GenerateDocumentBase64',
    {
      method: 'POST',
      headers: {
        client_id: process.env.FOXIT_DOCGEN_CLIENT_ID!,
        client_secret: process.env.FOXIT_DOCGEN_CLIENT_SECRET!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        base64FileString: TEMPLATE_B64,
        documentValues: deal,
        outputFormat: 'pdf',
      }),
    },
  );
  const { base64FileString } = await res.json();
  return base64FileString;
}

The eSign wait is a hook, not a poll. Define a hook, create it keyed by the folderId, and resume it from an API route when Foxit POSTs the completion webhook. The workflow pauses without consuming compute and resumes exactly where it left off.

// workflows/hooks.ts
import { defineHook } from "workflow";

export type FoxitCompletion = {
  event_name: string;
  data: { folder: { folderId: number; folderStatus: string } };
};

export const signatureHook = defineHook<FoxitCompletion>();
// app/api/foxit-webhook/route.ts
import { signatureHook } from "@/workflows/hooks";

export async function POST(req: Request) {
  const payload = await req.json();
  const folderId = payload?.data?.folder?.folderId;
  // Resume only on the terminal event, so earlier lifecycle events
  // (viewed, signed, completed) do not consume the single-use hook.
  if (folderId && payload?.event_name === "folder_executed") {
    await signatureHook.resume(String(folderId), payload);
  }
  return new Response("OK");
}

In this code, the workflow calls two steps and then waits on signatureHook. When Foxit’s folder_executed webhook hits /api/foxit-webhook, the route calls hook.resume with the folderId as the key, which wakes the paused workflow and hands it the payload so it can archive the signed document. The event_name check matters, because the same account webhook also delivers folder_viewed, folder_signed, and folder_completed, and any of those would otherwise consume the single-use hook early. Use sleep() from the workflow package for a timeout fallback if the signer never acts. The credential and endpoint facts are identical to the n8n version, since only the orchestration layer changed. The Workflow Concepts page documents the directives, sleep, and hooks in full, and the full runnable project is in the demo repo.

Deployed to Vercel and triggered with a sample deal, the run shows up under Observability, where you can watch runs pause and resume in real time.

Vercel Workflows dashboard run list

The Workflows runs list in the Vercel dashboard. A run stays Active while it is paused on the signing hook, then flips to Completed once the eSign webhook resumes it.

Opening a completed run shows the whole durable pipeline as a trace. The two Foxit steps run in seconds, the hook span (named by the folderId) accounts for the multi-minute wait while the document sat unsigned, and archiveSigned runs the instant the real folder_executed webhook resumes the workflow.

Vercel Workflows run trace with signing wait

A completed run trace. generateContract (1.77s) and sendForSignature (1.09s) are the real Foxit calls, the 34624119 span is the hook paused for about three minutes until the document was signed, and archiveSigned (680ms) downloads the executed PDF once the webhook fires.

CRM-Triggered Document Pipeline FAQ

Yes. Set outputFormat to "docx" to get a merged Word document back instead of a PDF. That’s useful for contracts that need further edits or approvals before going to eSign.

Yes. createfolder accepts a PDF from any source, such as a publicly accessible URL, multipart form data, or a base64 string you’ve encoded from an existing file. DocGen and eSign are independent APIs that pair well together because DocGen’s base64 output is exactly what createfolder’s inputType: “base64” mode expects, but neither requires the other.

The folder stays in SHARED or PARTIALLY_SIGNED status indefinitely with no automatic expiry. You’re responsible for detecting stalled folders. In n8n, use a Wait node with a timeout. In Vercel Workflows, use sleep() inside a Promise.race. Both paths let you branch to a follow-up action rather than letting the workflow hang.

Add multiple objects to the parties array with sequential sequence values (1, 2, 3…). The eSign API dispatches to party 2 only after party 1 has signed. Make sure each Text Tag in your template references the correct party number (${signfield:2:y} for party 2’s signature field) and that parties includes a matching entry for every number you use.

The token response includes expires_in in seconds. Cache the token with a timestamp, check before each call whether you’re within a safe margin of expiry (60 seconds is reasonable), and re-fetch from POST /api/oauth2/access_token when needed. Fetching a new token on every API call adds unnecessary latency and wastes credits.

Yes. base64FileString is an array. Pass multiple base64-encoded PDFs alongside their corresponding fileNames, and all documents will be included in the same folder for the signer to complete in one session.

A CRM-triggered document pipeline is an automated workflow that listens for a CRM event, such as a deal moving to “closed won” in HubSpot, and automatically generates, sends for signature, and archives a contract without human intervention. It matters because manual document handling after a deal closes introduces delays, missed steps, and no audit trail when something fails silently mid-process.

Test End-to-End and Harden for Production

Prove the pipeline by firing a sample deal at the n8n Webhook Test URL with the cURL command from Step 2. Confirm the Document Generation call returns a base64 PDF, the eSign folder is created with a folder.folderId, and the signing email reaches your test address. Running the middle by hand first means you catch a bad credential or a tag mismatch before any nodes downstream depend on it.

Harden the credentials next. Store client_id, client_secret, and the eSign keys as n8n credentials or Vercel environment variables, never as plaintext in node config, and add an n8n error workflow that alerts on any 4xx or 5xx from either Foxit API. Distinguish a 4xx (fix the payload) from a 5xx (retry with backoff).

Finally, size for credits. Each Document Generation call and each eSign send consumes credits, so read the Foxit API credits reference before going to production. The free Developer plan covers 500 credits per year, which is plenty to build and validate the whole pipeline.

Create a free Foxit developer account with no credit card required, grab your Client ID and Secret from the APIs Dashboard, and run your first Document Generation call against the live endpoint before wiring it into a workflow. Create your free account to get started.

Build a CRM-to-eSign Document Pipeline in n8n Using Foxit’s REST APIs

Illustration of a CRM deal moving through Foxit API steps to produce a signed PDF

This tutorial shows how to wire the Foxit API n8n integration into a single automated chain. You’ll learn how to generate a PDF, send it for signature, and archive the signed copy without anyone touching a file.

A deal moves to closed-won in the CRM, and then a person takes over. Someone exports the deal fields, pastes them into a contract template, saves a PDF, uploads it to a signing tool, types in the counterparty’s email, and hits send. Days later they check whether it was signed, download the executed copy, and drop it into a shared drive so finance can find it. Every one of those steps is a place where data gets retyped, a wrong template gets attached, or a signature request stalls because nobody was watching.

The failure is structural, not human. The deal data lives in the CRM, the contract template lives in a content folder, the signature lives in a separate vendor, and the archive lives somewhere else again. Nothing connects them, so a person becomes the integration layer.

The target state removes that person from the loop. A CRM event fires, an automation runs a short chain of API calls, and a signed PDF lands in storage with no manual touch. This tutorial builds exactly that using n8n, a source-available workflow tool, as the orchestrator. You will build it step by step, and you do not need to be an n8n expert to follow along. The chain is a CRM webhook, then document generation, then send-for-signature, then an archive step, wired together with n8n’s HTTP Request node.

A Continuous Chain of REST Calls

The pipeline has three stages, and each is a REST call the previous stage feeds directly.

  1. Generate. A CRM webhook delivers deal data into n8n. The workflow merges that data into a Word template and renders a PDF through the Foxit Document Generation API, a cloud REST endpoint with no SDK to install.
  2. Sign. The rendered PDF goes straight to the Foxit eSign API, which creates a signing folder, routes it to the parties, and manages the signature lifecycle.
  3. Archive. When signing completes, the workflow downloads the executed PDF and writes it to storage, then updates the CRM record.

The data handoff between stages is what makes the chain clean. Document Generation returns the finished PDF as a base64 string in its JSON response, and that same base64 string is exactly what the eSign folder-create call accepts as its file payload. No temporary files, no disk writes, and no format juggling between the two calls. The eSign completion event then carries the folder reference the archive stage uses to pull the signed document.

One detail belongs up front, since it shapes every node. The two APIs live on two different hosts. Document Generation runs on https://na1.fusion.foxit.com, and eSign runs on https://na1.foxitesign.foxit.com. They also authenticate differently, which the next section covers. There is no dedicated Foxit node in n8n and none is needed, because both are well-formed REST endpoints that the HTTP Request node handles with custom headers and JSON bodies.

Run n8n Locally and Get Foxit Credentials

The whole tutorial runs on free, self-hosted infrastructure. There is no n8n Cloud plan to sign up for and no paid Foxit tier involved. You run n8n yourself in a container, and the only account you create is Foxit’s free Developer plan.

Install Docker

You need Docker to run n8n. On macOS or Windows, install Docker Desktop; on Linux, install Docker Engine. Both are covered on the Get Docker page. After installing, confirm it works by running docker --version in a terminal.

Start n8n and create your local account

Run the two commands below, taken from n8n’s Docker docs:

docker volume create n8n_data
docker run -it --rm --name n8n -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n

The n8n_data volume persists your workflows and credentials across restarts, since n8n stores them in a local SQLite database by default. The -p 5678:5678 flag maps the editor to a local port. Now do these steps in order:

  1. Open http://localhost:5678 in your browser.
  2. On first launch, n8n asks you to create an owner account. This is a local account for your own instance, not an n8n Cloud login, so use any email and password you like.
  3. You land on an empty workflow canvas. This is where you will build the pipeline.

Swap -it --rm for -d in the run command when you later want n8n running in the background.

Get your Foxit credentials

Register a Foxit account at account.foxit.com/site/sign-up, verify your email, and activate the free Developer plan. From the APIs Dashboard, copy your Document Generation Client ID and Client Secret. The eSign API is provisioned separately, so once eSign is active on your account, retrieve its Client ID and Client Secret as a distinct pair. You now have two credential sets, and they are not interchangeable.

They differ because the two APIs authenticate differently, and this is the single thing that trips up most first builds. Document Generation takes the credentials as two request headers, lowercase client_id and client_secret, on every call. eSign instead runs an OAuth2 exchange first, trading its credentials for a short-lived bearer token that you then send as an Authorization: Bearer <token> header.

Store the DocGen credentials once, reuse them everywhere

Rather than pasting your Client ID and Secret into every node, store them once in n8n’s credential store. Because Foxit needs two custom headers, use the Custom Auth credential type (the same type Foxit’s own published n8n template uses). In n8n, go to Credentials, click Create credential, search for Custom Auth, click Continue, and paste this JSON:

{
  "headers": {
    "client_id": "YOUR_FOXIT_CLIENT_ID",
    "client_secret": "YOUR_FOXIT_CLIENT_SECRET"
  }
}
n8n Custom Auth credential form with Foxit client_id and client_secret JSON fields

The Custom Auth credential holds both Foxit headers in one place. Any HTTP Request node can attach it, so your Client ID and Secret never appear in a node field or an exported workflow.

In this credential, the headers object lists the two headers Foxit expects, with your real Client ID and Secret as the values. Save it, and every Document Generation call in this tutorial can attach it instead of carrying the raw keys.

Import the Finished Workflow (Optional Shortcut)

If you would rather see the whole pipeline first and study it, import the finished version and then follow the step-by-step build below to understand each node. On any workflow canvas, open the (Actions) menu in the top right and choose Import from URL….

n8n workflow actions menu with Import from URL option highlighted

The Actions menu on the workflow canvas. “Import from URL…” pulls a workflow straight from a link, so there is nothing to download.

Paste the raw URL of the ready-made pipeline JSON and click Import:

https://github.com/lucienchemaly/foxit-demo-templates/raw/main/n8n/foxit-crm-to-esign-pipeline.json
n8n Import Workflow from URL dialog with GitHub link pasted in

The Import from URL dialog. n8n fetches the JSON and renders the full node graph on the canvas, ready for you to attach your own credentials.

The imported graph arrives with placeholder credentials, so you still create the Custom Auth credential above and select it on the two Foxit nodes. Whether you import or build from scratch, the sections below explain exactly what each node does.

Step 1: Trigger the Workflow and Generate the PDF

Add the trigger and fire it with a test request

Start the workflow with a Webhook node, which is how a CRM will eventually call your pipeline when a deal closes. Click the + on the canvas, search for Webhook, and add it. Set HTTP Method to POST and Path to crm-deal-closed. n8n gives every Webhook node two addresses, a Test URL for building and a Production URL for live traffic.

The Webhook node’s Test URL and “Listen for test event” button. Clicking Listen puts the node in a one-shot capture mode so you can send it a sample request while building.

Because you are working locally and do not have a real CRM pointed at your laptop yet, fire the webhook yourself. Click Listen for test event, copy the Test URL, and send it a sample deal payload with cURL:

curl -X POST "http://localhost:5678/webhook-test/crm-deal-closed" \
  -H "Content-Type: application/json" \
  -d '{
    "company_name": "Acme Robotics",
    "deal_id": "INV-2026-0042",
    "amount": "1875.50"
  }'

In this request, you POST a small JSON object that mimics what a CRM would send when a deal closes. The Webhook node captures it, and those three fields (company_name, deal_id, amount) become available to every downstream node as {{ $json.body.company_name }} and so on. When you later move to production, switch to the node’s Production URL and register it in your CRM’s webhook settings. To expose your local machine to the internet for that, run a tunnel such as ngrok (ngrok http 5678) and register the tunnel address.

Prepare the Word template

The template is an ordinary Word document with Foxit DocGen merge tags where the deal data should land. A tag is a field name in double braces, and you can add Word-style format switches for dates and currency. Download the ready-to-use sample so you do not have to author one, invoice_simple.docx. It carries {{ companyName }}, {{ invoiceNumber }}, {{ invoiceDate \@ MM/dd/yyyy }}, and {{ totalDue \# "$#,##0.00" }}. For repeating rows such as line items, DocGen supports a loop with {{TableStart:lineItems}} and {{TableEnd:lineItems}} markers placed in the same table row, shown in the companion invoice_table.docx. If you want to confirm a template’s tags before wiring it in, POST it once to https://na1.fusion.foxit.com/document-generation/api/AnalyzeDocumentBase64, which returns the full list of tags it detected. The Foxit DocGen quickstart walks through the request shape in more detail.

Add and configure the Generate PDF node

Click + to add an HTTP Request node after the Webhook, and configure it field by field.

  • Method : set to POST.
  • URL : https://na1.fusion.foxit.com/document-generation/api/GenerateDocumentBase64
  • Authentication : choose Generic Credential Type, then Custom Auth, and select the credential you created earlier.
  • Send Body : turn this on, set the body type to JSON, and paste the payload below.
n8n HTTP Request node configured with the Foxit Document Generation API URL

The HTTP Request node config panel. Every Foxit call in this tutorial is built here by setting the method and URL, attaching the credential, and toggling Send Body on for the JSON payload.

{
  "base64FileString": "<base64-encoded invoice_simple.docx>",
  "documentValues": {
    "companyName": "{{ $json.body.company_name }}",
    "invoiceNumber": "{{ $json.body.deal_id }}",
    "invoiceDate": "2026-07-01",
    "totalDue": "{{ $json.body.amount }}"
  },
  "outputFormat": "pdf"
}

In this body, base64FileString is the Word template encoded as base64, documentValues is a flat object whose keys match the template tags exactly and whose values are pulled from the webhook payload using n8n expressions, and outputFormat is the lowercase string "pdf". The keys in documentValues must match the tag names in the template character for character, since a mismatch renders that field empty rather than raising an error. To turn the template file into the base64 string, fetch it with an HTTP Request node set to return the file, then convert it in a Code node with items[0].binary.data.data, or paste a pre-encoded string while you are testing.

One limit is worth planning for. Document Generation rejects a .docx payload larger than 4 MB measured after base64 encoding, which is roughly a 3 MB raw file, and the rejection comes back as an HTTP 500 rather than a friendly validation message. If your template is heavy, compress its images through Word’s Picture Format pane, drop embedded fonts and OLE objects, and split an oversized template into parts you merge later.

The endpoint is synchronous, so there is no polling. The response is a JSON object with message, fileExtension, and base64FileString, where base64FileString holds the rendered PDF as base64. That value flows straight into Step 2.

Step 2: Exchange an eSign Token and Send for Signature

Get a bearer token

The eSign leg starts by turning your eSign credentials into a bearer token. Add another HTTP Request node and configure it:

  • Method : POST
  • URL : https://na1.foxitesign.foxit.com/api/oauth2/access_token
  • Send Body : on, body type Form Urlencoded, with four fields, grant_type set to client_credentials, client_id set to your eSign Client ID, client_secret set to your eSign Client Secret, and scope set to read-write.

The response is JSON with access_token, token_type (the string bearer), expires_in, and instance_url. This is a standard OAuth2 client-credentials grant as defined in RFC 6749. The token is long-lived but not permanent, so in production cache it and refresh when expires_in is close to elapsing rather than minting a new one on every run.

The token node uses a Form URL Encoded body with the four OAuth fields, not JSON. This is the one eSign call that sends the raw credentials rather than a bearer header.

Click Execute step on this node to confirm it works before wiring the rest. A successful run returns the token in the output panel.

A successful token call. The green check and the access_token / token_type values confirm the credentials and endpoint are correct before you build the next node.

Place signature fields with Text Tags

Signature placement is handled inside the document using Foxit eSign Text Tags, which you type into the Word template while authoring it. The syntax is ${fieldtype:party:mandatory}, so ${signfield:1:y} is a required signature for party 1 and ${datefield:2:n} is an optional date for party 2. If a tag needs to contain a space, replace it with an underscore, because a literal space breaks tag recognition. To keep the tags invisible in the finished document, set their text color to match the page background. On upload you set processTextTags: true, and Foxit converts each tag into a real signing field automatically, which removes any manual drag-and-drop field placement.

There is a party-mapping rule that fails silently, so check it before every send. Every party number referenced by a Text Tag must have a matching entry in the parties array of the create call. If a tag points at party 2 but your parties array only defines party 1, a sendNow: true create still returns success while quietly dropping party 2’s fields, so nobody is ever asked to sign them. Line up the token party numbers with the parties entries first.

Add the Create Signature Folder node

Add a third HTTP Request node for the send-for-signature call:

  • Method : POST
  • URL : https://na1.foxitesign.foxit.com/api/folders/createfolder
  • Send Headers : on, add one header named Authorization with the value Bearer {{ $json.access_token }}, which references the token from the previous node.
  • Send Body : on, body type JSON, with the payload below.
{
  "folderName": "Contract - {{ $('CRM Webhook').item.json.body.company_name }}",
  "inputType": "base64",
  "base64FileString": ["{{ $('Generate PDF').item.json.base64FileString }}"],
  "fileNames": ["contract.pdf"],
  "processTextTags": true,
  "sendNow": true,
  "parties": [
    {
      "firstName": "Alex",
      "lastName": "Rivera",
      "emailId": "[email protected]",
      "permission": "FILL_FIELDS_AND_SIGN",
      "sequence": 1,
      "workflowSequence": 1
    }
  ]
}

In this call, inputType is set to "base64" and base64FileString is an array holding the PDF that Step 1 produced, which is the direct handoff between the two APIs. The parties array lists each signer with their name, email, and order, processTextTags: true turns the embedded tags into fields, and sendNow: true dispatches the folder for signature in the same request. The response nests the identifier at folder.folderId, which you keep for status tracking and archival. Foxit’s API uses folder terminology throughout rather than envelope. If your process needs a review gate before anything is sent, set sendNow: false to create a draft, then dispatch it later with a POST /api/folders/sendDraftFolder call carrying the returned folderId.

n8n HTTP Request node with Authorization bearer header for the Foxit createfolder API

The create-folder node. The Authorization header value Bearer {{ $json.access_token }} pulls the token straight from the previous node, and the input panel on the left shows that token flowing in.

n8n canvas showing the CRM webhook, Generate PDF, eSign token, and Create Signature Folder nodes connected

The finished chain in the local n8n editor. Each stage is a plain HTTP Request node, and the base64 PDF from the generation node flows directly into the eSign create-folder node.

Once the folder is sent, the signer receives the document with the Text Tags already converted into interactive fields routed to them.

What the recipient sees after dispatch. This is the visible proof that processTextTags placed the fields correctly and routed them to the right party.

Step 3: Capture Signing Status and Archive the Document

With the folder sent, the workflow needs to know when it is signed so it can archive the result. The folder moves through a defined lifecycle, DRAFT, then SHARED, then PARTIALLY SIGNED, then COMPLETED, then EXECUTED, where the digital signature is applied to the PDF a few seconds after COMPLETED. Archive on the COMPLETED or EXECUTED event so you capture the finalized, digitally signed file.

Foxit eSign can push a webhook when the folder completes, configured in the eSign portal’s owner-only API and Webhooks settings, and you would receive it with a second n8n Webhook node. For a first local build, the simpler path is to poll. Add a Schedule Trigger node paired with an HTTP Request to https://na1.foxitesign.foxit.com/api/folders/viewActivityHistory?folderId={id}, which reads the folder’s audit trail (Created, Invitation Sent, Opened, Viewed, Signed, Folder Executed). This endpoint is GET-only and returns data only once the folder has been shared or sent, so it will not respond for a draft. Gate the next step with an IF node that checks whether the status has reached COMPLETED.

On completion, download the signed PDF through the eSign API and route the binary to a storage node, whether that is Google Drive, an S3 bucket, or an internal file-system node, then fire any downstream updates such as a Slack message or a CRM field write. Because not every send completes, add an error branch as well. If a party declines or the folder expires, route to a separate path that updates the CRM record to reflect the outcome and alerts the deal owner, so a human steps in only on the exceptions.

Common Mistakes and Troubleshooting

A few pitfalls account for most failed first runs.

  • Wrong host. Document Generation is on na1.fusion.foxit.com; eSign is on na1.foxitesign.foxit.com. Sending a DocGen body to the eSign host (or the reverse) returns an auth or not-found error.
  • Credential not attached. If a Foxit call comes back 401, the most common cause is an HTTP Request node with Authentication left on None. Attach the Custom Auth credential (DocGen) or add the Authorization: Bearer header (eSign).
  • Token expired or missing. The eSign bearer token is not permanent. If createfolder returns 401 after the pipeline has been idle, re-run the token node so a fresh access_token flows into the header.
  • Tag names do not match. A documentValues key that does not exactly match a {{ }} tag renders that field blank with no error. Cross-check the names, or run AnalyzeDocumentBase64 to list the real tags.
  • Base64 payload too big. An HTTP 500 with a “cannot be larger than 4 MB” body means the encoded .docx exceeded the cap. Slim the template per Step 1.
  • Party numbers do not line up. A Text Tag pointing at a party not present in the parties array is silently dropped, so that signer is never asked to sign. Match token party numbers to parties entries.

Foxit API n8n FAQ

The free Foxit Developer plan gives you 500 credits per year with instant activation and no credit card required. n8n runs as the source-available Community Edition in Docker with no license key. Both are fully functional for building and testing the complete pipeline described here.

No. Each API uses its own Client ID and Secret. You will store two separate credential sets in n8n, one for the Document Generation header-based auth and one for the eSign OAuth2 token exchange. They come from separate sections of the Foxit APIs Dashboard.

Use an n8n Schedule node as your trigger. Add an HTTP Request node after it that polls your CRM’s REST API for deals matching your target stage criteria. HubSpot’s Deal Search API and Salesforce’s SOQL query endpoint both support this pattern. The rest of the workflow runs identically from that point forward.

Yes. If you prefer not to embed Text Tags in the DOCX, omit processTextTags: true from your createfolder call and place fields manually using the Foxit eSign web editor after the document is uploaded. The manual approach works fine for low-volume workflows but does not scale as cleanly as tag-based automation when the template changes frequently.

A folder moves through five states in order:

  • DRAFT : created but not sent
  • SHARED : sent to all parties
  • PARTIALLY SIGNED : at least one party has signed
  • COMPLETED : all parties have signed
  • EXECUTED : the digital signature stamp is applied

Your n8n archival step should trigger on COMPLETED or EXECUTED. If you archive on COMPLETED and need the final stamp, wait for the EXECUTED event instead.

The expires_in field in the token response tells you how long the bearer token is valid. For high-frequency workflows, add a Code node before the createfolder call that checks whether the stored token is still valid and re-requests it if not. You can also request a fresh token at the start of each pipeline execution. The client-credentials grant is stateless, so there is no session overhead to worry about.

Run your template through the Analyze endpoint at POST https://na1.fusion.foxit.com/document-generation/api/AnalyzeDocumentBase64 before executing the full pipeline. It returns every tag the API detects, making it straightforward to catch case mismatches or typos. The merge API performs a case-sensitive lookup, so a template field named {{First_Name}} will not match a JSON key of first_name.

What to Build Next

The fastest way to prove the pipeline is to run the middle of it by hand first. Spin up the local n8n container, activate the free Foxit Developer plan, pull your Client ID and Secret from the APIs Dashboard, and fire the Document Generation POST against invoice_simple.docx from a REST client like Postman or cURL. Once that returns a base64 PDF, you know the hardest call works before any nodes are wired.

From there, two durable extensions are worth building. Save your signing setup as a reusable eSign template with POST /api/templates/createtemplate so future sends skip the field setup, and version the n8n workflow itself by exporting its JSON, so the whole chain can be redeployed or shared across the team.

Building Agentic Document Workflows: How LLM Agents Use PDF APIs to Convert, Extract, and Sign at Scale

Diagram of agentic document workflows showing an LLM agent calling Foxit MCP tools to convert, extract, and sign PDFs

This guide walks through building agentic document workflows by exposing Foxit’s PDF and eSign APIs as callable MCP tools, so any compatible agent host can run the full document lifecycle in one automated pipeline.

Agentic document workflows go beyond retrieval, since they convert, transform, merge, and sign documents without human intervention. This guide shows how to expose Foxit’s production PDF API as callable MCP tools so any LLM agent can execute the full document lifecycle (OCR, extraction, generation, and legally binding signatures) in a single automated pipeline.

Most LLM-powered applications have solved the retrieval problem. The harder part of agentic document workflows is action, the moment your agent needs to convert a scanned invoice to searchable text, merge a dozen contract pages into a package, and route it for a legally binding signature without a human in the loop.

RAG gets text into a context window, which is useful for reading. Once you need to produce, transform, or sign a document, you’ve moved into document operations territory. A plain text API won’t close that delta, and bolting together a dozen bespoke REST wrappers every time you need a new pipeline quickly becomes the bottleneck.

The Model Context Protocol (MCP) gives agents a standard way to discover and call tools. Document workflows have been missing a tool surface that exposes real PDF operations as callable MCP tools, backed by a production-ready API. This guide walks through exactly how to build that.

What You Need Before You Start

Five prerequisites are required to follow this guide. You need a Foxit developer account, the open-source MCP server, an MCP-compatible host, three environment variables, and a Python workspace for the signing example.

A Foxit developer account. Sign up at account.foxit.com/site/sign-up (no credit card required for the free Developer plan). The Foxit Developer Portal issues your Client ID and Client Secret, gives you access to the API Playground, and tracks usage in real time.

The open-source MCP server. Clone github.com/foxitsoftware/foxit-pdf-api-mcp-server. The repo ships two active implementations, a Python build (using FastMCP, Python 3.11+, and the uv package manager) and a TypeScript build (Node.js 18+, pnpm). The original stdio-python variant is deprecated, so use the current Python or TypeScript implementation.

An MCP-compatible host. You need somewhere to run the agent. Claude Desktop, Cursor, or VS Code with GitHub Copilot all work, and any MCP-compliant custom agent framework will also connect to the server.

A Python workspace for the signing example. The eSign walkthrough later in this guide runs a short Python script, so you need Python 3.8+ and the requests library. That walkthrough also uses a separate set of eSign credentials, which you set up in its own section rather than here. Scaffold an isolated workspace in one shot:

mkdir agentic-docs && cd agentic-docs
python3 -m venv .venv && source .venv/bin/activate
pip install requests

Three environment variables. Before launching your MCP host process, export these:

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"

Never hardcode credentials in config files. The MCP server reads these at startup and uses them to authenticate every request to the PDF Services API.

What “Agentic” Actually Means for Document Workflows

An agentic document workflow executes operations on documents (converting formats, applying OCR, merging pages, routing for signature) rather than simply retrieving text from them. The tool surface required is fundamentally different from a RAG setup.

Retrieval-augmented generation pulls text from a document and injects it into a prompt. An agentic document workflow does something to a document, whether it converts a format, applies OCR to make a scanned image searchable, merges pages from multiple sources, or routes the result for signature.

In a tool-use architecture, the LLM doesn’t call the API directly. It picks the right operation from a catalog of tools based on the task, calls it with structured inputs, processes the result, and decides whether to continue the chain or hand off to the next step. If you’ve worked with web-search or code-execution tools in LangChain or AutoGen, the pattern is identical. The model reasons about which tool to invoke, not about how the underlying HTTP request works.

A REST API is an HTTP surface. An MCP tool is a named, typed function with an input schema, an output contract, and a description the model uses to decide when and whether to call it. MCP standardizes that interface so any compliant host can discover the full tool catalog, call individual operations, and chain results without custom adapter code.

A well-designed MCP server eliminates the bespoke integration layer. Without one, every document-heavy agent pipeline requires someone to write and maintain that plumbing from scratch.

Architecture Overview: Two Modes for Agent-Driven PDF Processing

The Foxit PDF API MCP Server wraps Foxit’s cloud PDF Services API as 30+ callable MCP tools, covering every stage of a document lifecycle. Foxit PDF Editor is the first PDF editor in the industry to act as an MCP Host, connecting outward to external MCP Servers and acting on open documents.

Those two facts define two distinct architectural modes.

Mode 1: Programmatic pipeline. Your MCP host (Claude Desktop, Cursor, VS Code with GitHub Copilot, or a custom agent) registers the Foxit PDF API MCP Server. The agent calls PDF tools directly, the server translates those calls into Foxit PDF Services REST requests, and structured results return to the agent. The agent never writes REST plumbing. This is the right model for automated pipelines running without a human in the loop.

Mode 2: In-app orchestration. Foxit PDF Editor acts as the MCP Host. Its embedded AI Assistant connects to external MCP Servers (Jira, Salesforce, Gmail, Notion, GitHub, Google Workspace) and acts on the open document. You could extract fields from a contract PDF and open a Jira ticket without leaving the editor. This is the right model when a knowledge worker needs AI assistance during document review.

Mode 1 is what the rest of this guide builds. Its data flow runs like this:

Foxit MCP server architecture diagram for agentic document workflows

The agent calls tools, the MCP server handles the REST layer against PDF Services, and a prepared document hands off to eSign at the end of the chain.

One detail to understand before you build is that each successful Foxit PDF Services API call consumes one credit from your plan. Failed requests (4xx or 5xx) do not consume credits. The Developer Dashboard shows real-time usage, so you can see exactly what a pipeline costs per document before scaling it up.

Setting Up the Foxit MCP Server

The server exposes tools across six categories (document lifecycle, creation, conversion, manipulation, security, and forms) plus OCR and document compare.

The full tool catalog breaks down as follows:

  • Document lifecycle : upload, download, delete
  • Creation : Word, Excel, PowerPoint, HTML, URL, plain text, and image to PDF
  • Conversion : PDF to Word, Excel, PowerPoint, HTML, plain text, and image
  • Manipulation : merge, split, extract pages, compress, flatten, linearize, watermark, and page operations
  • Security : add and remove passwords, set permissions
  • Forms : export and import form data as JSON

OCR and document compare are also in the catalog. Signing lives in the eSign API covered in Section 6.

Mode 1: Programmatic Pipeline Setup

Clone the repo and pick your implementation. For the Python version with VS Code and GitHub Copilot, create or update your .vscode/mcp.json with the following:

{
  "servers": {
    "foxit-pdf": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/foxit-pdf-api-mcp-server",
        "run",
        "foxit-pdf-api-mcp-server"
      ],
      "env": {
        "FOXIT_CLOUD_API_HOST": "${env:FOXIT_CLOUD_API_HOST}",
        "FOXIT_CLOUD_API_CLIENT_ID": "${env:FOXIT_CLOUD_API_CLIENT_ID}",
        "FOXIT_CLOUD_API_CLIENT_SECRET": "${env:FOXIT_CLOUD_API_CLIENT_SECRET}"
      }
    }
  }
}

VS Code launches the MCP server as a subprocess through uv, which runs the cloned Python build from its directory. The three ${env:...} references pull credentials from the shell environment instead of hardcoding them in the file, so replace /absolute/path/to/foxit-pdf-api-mcp-server with the path where you cloned the repo and restart VS Code to load the server. Claude Desktop uses the same shape under an mcpServers key and can run the published npm package directly with "command": "npx" and "args": ["-y", "@foxitsoftware/foxit-pdf-api-mcp-server"], so you do not have to clone anything for that host.

Mode 2: In-App Orchestration Setup

Open Foxit PDF Editor, click the AI Assistant tab in the Ribbon, and launch AI Chat to open the right-hand panel. In the bottom left of that panel, click MCP Tools, then click Add MCP Server to configure a new MCP service. Fill in the required fields and save. Once configured, the server appears in the MCP Tools list and its tools activate inside AI Chat.

In both modes, the server reads your Client ID and Client Secret from the environment variables set at startup. No additional gateway configuration is required.

Core Document Operations Agents Can Execute

With the server running, your agent has 30+ callable PDF operations available. Five categories do the heaviest lifting in production pipelines, namely conversion, OCR, structural extraction, merge/split, and document generation via DocGen.

Conversion. An agent receiving an uploaded DOCX triggers the Word-to-PDF creation tool before any downstream step. The output is a standards-compliant PDF that every subsequent operation (OCR, extraction, merge) can work with consistently, eliminating manual conversion and format ambiguity downstream.

OCR. When an agent ingests a scanned or image-only PDF, an OCR call should precede any extraction step. Calling the OCR tool makes the document searchable and text-extractable, which is required for invoice and contract pipelines where key fields sit inside scanned images. The agent calls OCR, waits for the result, and proceeds.

Structural extraction. After OCR, an agent extracts text, tables, and form-field data as structured JSON. Foxit’s structural extraction returns per-page content plus images, giving you a payload that routes cleanly into a BI tool or a second LLM step for analysis or classification. For the full response schema, refer to the Foxit MCP Server developer blog.

Merge and split. An agent assembling a contract package from multiple source documents calls the merge tool with an ordered list of PDFs. An agent pre-processing a large compliance document for parallel LLM analysis calls the split tool to divide it into per-section chunks. Both operations are synchronous and safe to retry on failure.

Document generation via DocGen. For dynamically generated contracts, invoices, or reports, the Foxit Document Generation API accepts a DOCX template with {{dynamic_tags}} and a JSON data payload from a CRM, database, or form response. It returns a finished PDF via POST /document-generation/api/GenerateDocumentBase64. A ready-to-use template lives in the Foxit demos repo if you want to test the call without authoring one. When you upload a DOCX template directly, the 4 MB post-base64 encoding cap applies, so slim templates down by stripping embedded fonts and large images before encoding. The agent supplies the JSON payload at runtime, so a single template can produce thousands of unique documents.

A representative end-to-end pipeline runs like this. An agent ingests a purchase order scan, calls OCR to make it searchable, extracts the structured fields as JSON, merges that data into a contract template via DocGen, and hands the finished PDF off to eSign. Every step is a tool call. The agent reasons about sequencing while the MCP server handles the REST layer.

Agent-Triggered Signing Workflows via the eSign API

Document signing uses a separate REST service, the Foxit eSign API, which has its own credentials and completes the pipeline in three calls. The agent exchanges its credentials for a token, creates a signing folder from the prepared PDF, and dispatches it to the signer.

The eSign API runs on its own host and issues its own Client ID and Client Secret from the eSign portal, separate from the PDF Services credentials the MCP server uses. Export the three eSign variables the script reads before running it:

export FOXIT_ESIGN_BASE_URL="https://na1.foxitesign.foxit.com"
export FOXIT_ESIGN_CLIENT_ID="your_esign_client_id"
export FOXIT_ESIGN_CLIENT_SECRET="your_esign_client_secret"

The folder can only be sent if the signer has a signature field, and the simplest way to place one is with Foxit eSign text tags embedded in the document. Download the ready-to-sign sample, agent_agreement.pdf, into your workspace as agreement.pdf. It already carries the tag that maps a signature field to the first party, so the folder is sendable as is. Then run:

import base64
import os
import requests

BASE_URL = os.environ["FOXIT_ESIGN_BASE_URL"]      # https://na1.foxitesign.foxit.com
CLIENT_ID = os.environ["FOXIT_ESIGN_CLIENT_ID"]
CLIENT_SECRET = os.environ["FOXIT_ESIGN_CLIENT_SECRET"]


def get_access_token():
    resp = requests.post(
        f"{BASE_URL}/api/oauth2/access_token",
        data={
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "grant_type": "client_credentials",
            "scope": "read-write",
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["access_token"]


def route_for_signature(pdf_path, signer):
    token = get_access_token()
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

    with open(pdf_path, "rb") as fh:
        encoded = base64.b64encode(fh.read()).decode()

    folder = requests.post(
        f"{BASE_URL}/api/folders/createfolder",
        headers=headers,
        json={
            "folderName": "Agent Service Agreement",
            "inputType": "base64",
            "base64FileString": [encoded],
            "fileNames": ["agreement.pdf"],
            "processTextTags": True,
            "sendNow": False,
            "parties": [
                {
                    "permission": "FILL_FIELDS_AND_SIGN",
                    "firstName": signer["first_name"],
                    "lastName": signer["last_name"],
                    "emailId": signer["email"],
                    "sequence": 1,
                }
            ],
        },
        timeout=60,
    )
    folder.raise_for_status()
    folder_id = folder.json()["folder"]["folderId"]

    sent = requests.post(
        f"{BASE_URL}/api/folders/sendDraftFolder",
        headers=headers,
        json={"folderId": folder_id},
        timeout=60,
    )
    sent.raise_for_status()
    return folder_id


if __name__ == "__main__":
    fid = route_for_signature(
        "agreement.pdf",
        {"first_name": "Jordan", "last_name": "Lee", "email": "[email protected]"},
    )
    print(f"Folder {fid} sent for signature")

In this code, you read the eSign credentials from the environment and exchange them for a bearer token at the access_token endpoint, where the request is form-encoded rather than JSON (sending JSON returns a 415). You then base64-encode the local PDF and post it to createfolder with inputType set to base64 so the API reads the base64FileString array, with processTextTags set to True so the document’s text tags become a real signature field, and with sendNow set to False so the folder is created as a draft instead of emailing anyone immediately. The parties array names the signer with FILL_FIELDS_AND_SIGN permission, you read the new id at folder.folderId, and you pass it to sendDraftFolder, which dispatches the draft to the signer. To confirm the result, call GET /api/folders/viewActivityHistory?folderId={id}, which returns the activity log once the folder has been shared (a draft returns logs of a non-shared folder can not be viewed). Foxit uses “folder” throughout the eSign API, never “envelope.”

Compliance is built into the API layer. The Foxit eSign API supports eIDAS, the ESIGN Act, UETA, HIPAA, and GDPR, covering the requirements for agents operating in legal, healthcare, and finance contexts. No separate compliance infrastructure is required.

Production Considerations: Compliance, Cost, and Error Handling

The Foxit PDF Services and eSign APIs are SOC 2 Type II certified, with HIPAA BAA support, GDPR compliance, and CCPA coverage built in. Three additional concerns (credit consumption, idempotency, and async polling) determine whether a pipeline runs reliably at scale.

Credit consumption. The free Developer plan includes 500 credits per year (annual reset, no rollover). The Startup plan is $1,750/year for 3,500 credits. The Business plan is $4,500/year for 150,000 credits. Each successful API call consumes one credit; 4xx and 5xx responses do not. A pipeline processing 50 documents per day burns through those allocations quickly. Monitor real-time usage in the Developer Dashboard before moving to production and size your plan accordingly.

Idempotency. Merge, flatten, and convert calls are safe to retry on failure. Signing-folder creation is not, so gate createfolder behind a state check so the agent doesn’t create duplicate folders and dispatch duplicate signing requests to the same signers.

Async operations. Translation and batch conversion operations are asynchronous, so they return a job ID immediately, and your agent should poll the job status endpoint roughly every three seconds until the status is COMPLETED or FAILED before proceeding to the next step in the chain. Handling these concerns at build time separates a working pipeline from one that generates support tickets.

Common Mistakes

  • Dropping the /api/ prefix on eSign calls : Every eSign path lives under /api/, as in /api/folders/createfolder. Omitting it returns a 404 against a docs-style path that does not exist.
  • Sending the token request as JSON : The access_token endpoint is form-encoded. A JSON body returns 415 Unsupported Media Type, so pass the credentials as form data.
  • Forgetting inputType: base64 : When you send a base64 PDF without it, createfolder rejects the request with fileUrls or base64FileString cannot be empty. URL mode uses fileUrls and fileNames instead.
  • Sending a signer with no signature field : A FILL_FIELDS_AND_SIGN party needs a field. If the document has no text tag like ${s:1:______} and you skip processTextTags, sendDraftFolder returns Please assign a signature field. Use underscores in the tag placeholder, since an empty placeholder does not create a field.
  • Expecting a signing tool in the MCP server : The 30+ MCP tools cover PDF operations, not signatures. Signing is the eSign API, a separate service with separate credentials.

Agentic Document Workflows FAQ

An agentic document workflow is an automated pipeline in which an LLM agent executes operations on documents (conversion, OCR, extraction, merging, generation, and signing) without human intervention. Unlike retrieval-augmented generation, which only reads documents, an agentic workflow produces and transforms them using callable tools exposed through a protocol like MCP.

MCP defines a standard interface for exposing named, typed functions, called tools, that an LLM agent can discover and invoke. A PDF API MCP server wraps REST endpoints as MCP tools with input schemas and output contracts. The agent selects the right tool based on task context, calls it with structured parameters, and processes the result, without writing any HTTP request logic.

The Foxit PDF API MCP Server exposes 30+ tools covering document lifecycle (upload, download, delete), creation (Word, Excel, PowerPoint, HTML, image to PDF), conversion (PDF to multiple formats), manipulation (merge, split, compress, OCR, watermark), security (password management), and forms (JSON import/export).

No. Only successful Foxit PDF Services API calls consume credits. Requests that return 4xx or 5xx status codes do not count against your plan. The Developer Dashboard provides real-time usage tracking so you can measure pipeline cost per document before scaling.

After preparing a document, your agent calls the Foxit eSign API directly. It authenticates with client_credentials, POSTs to /api/folders/createfolder with the base64 PDF and a parties entry for the signer, then POSTs to /api/folders/sendDraftFolder with the returned folderId. The signing email workflow triggers automatically, and the agent can poll /api/folders/viewActivityHistory for the audit trail.

The Foxit eSign API supports eIDAS, the U.S. ESIGN Act, UETA, HIPAA, GDPR, and CCPA. The PDF Services API is SOC 2 Type II certified with HIPAA BAA support. No separate compliance wrappers are required for agents operating in legal, healthcare, or financial contexts.

Mode 1 is a programmatic pipeline where an external LLM agent (Claude Desktop, Cursor, VS Code with GitHub Copilot, or a custom framework) registers the Foxit MCP Server and calls PDF tools automatically. Mode 2 is in-app orchestration where Foxit PDF Editor acts as the MCP Host, connecting to external services like Jira or Salesforce while a knowledge worker reviews the open document.

The /api/folders/createfolder endpoint is not idempotent. If an agent retries on failure without a state check, it will create duplicate folders and send duplicate signing requests to the same signers. Merge, flatten, and convert operations are safe to retry; folder creation requires the agent to verify no existing folder was created before calling the endpoint again.

Start Building: Free Developer Access in Minutes

The full pipeline in this guide is available to test today. Activate a free Foxit Developer plan to get your Client ID, Client Secret, access to the API Playground, and 500 credits for real requests. No credit card required.

Clone the open-source MCP server, export the three environment variables, and register the server in Claude Desktop, Cursor, or VS Code with GitHub Copilot. At that point, 30+ PDF tools are callable from your agent with no local SDK to install and no REST plumbing to write.

Once a document is prepared, extend the pipeline into legally binding signatures with the eSign API. The architecture in this guide covers the full document lifecycle from conversion and OCR through extraction, generation, merging, and signing, all in a single agentic document workflow.

Create your free developer account to clone the open-source MCP server and make 30+ PDF tools callable from your agent in minutes, no credit card required.

Create Custom Invoices with Word Templates and Foxit Document Generation

Word invoice template with double-bracket tokens used by a generate invoice pdf API

This tutorial shows how to design a Word invoice template with dynamic tokens and use Foxit’s Document Generation API and a short Python script to turn JSON data into polished, ready-to-send PDF invoices.

Manual invoice processing costs between $18 and $26 per invoice, while automated workflows bring that down to $2.50 to $4, according to industry accounts-payable benchmarks. Foxit’s Document Generation API generates well-formatted, dynamic PDF invoices from your existing data, which eliminates manual data entry, formatting errors, and the overhead of per-invoice handling. This tutorial shows you how to build that workflow end to end.

Before You Start

To follow along, grab your free credentials on the developer portal and read the introductory guide to automated document pipelines, which covers the basics of working with the API.

The API works with Microsoft Word templates containing tokens wrapped in double brackets. You pass the template and your data to the API, it replaces those tokens with your data values, and returns a PDF (or Word file, if you prefer).

Prerequisites

This tutorial runs a short Python script, so set up the following before you start:

  • Python 3.12 or newer. The script uses an f-string syntax introduced in Python 3.12.

  • pip for installing packages, plus the built-in venv module for an isolated environment.

  • The requests library for the HTTP call.

  • A code editor such as VS Code with the Python extension. PyCharm, WebStorm, or Sublime Text work too.

  • curl if you want to hit the endpoint from the command line first.

  • A Foxit developer account. Sign up for free to get your Client ID and Client Secret.

Scaffold the workspace in one shot:

mkdir foxit-invoices && cd foxit-invoices
python3 -m venv .venv
source .venv/bin/activate
pip install requests

Then export your credentials and host so the script can read them from the environment:

export CLIENT_ID="your_client_id"
export CLIENT_SECRET="your_client_secret"
export HOST="https://na1.fusion.foxit.com"

Designing Your Invoice Word Template

Template design in Word is your starting point. An invoice typically includes:

  • The customer receiving the invoice

  • The invoice number and issue date

  • The payment due date

  • A line-item table with product name, quantity, price, and a running total

The Document Generation API places no constraints on template design. Size, alignment, and styling can match your corporate standards, whether that means a simple layout or a polished, branded one. Consider the template below (download link at the end of this article):

MS Word template

The Word template with double-bracket tokens that the Document Generation API replaces at runtime.

Working from the top of the template:

  • {{ invoiceNum }} is the invoice number for the customer.

  • {{ today \@ MM/dd/yyyy }} combines a built-in value (today, which represents the current date at API call time) with a date mask that controls the display format. The API docs list all available masks.

  • {{ accountName }} is a standard token mapped directly from your data.

  • {{ paymentDueDate \@ MM/dd/yyyy }} demonstrates date masks applied to dates from your own data, not just built-ins.

  • The line-item table uses one header row and one data row. The dynamic row opens with {{TableStart:lineItems}} (where lineItems is the name of an array in your JSON) and closes with {{TableEnd:lineItems}}. Between those markers sit product, qty, price, totalPrice, and ROW_NUMBER (a built-in that auto-increments from 1). The \# Currency format applied to totalPrice renders it as a formatted currency value.

  • A final table row uses SUM(ABOVE) with currency formatting to total the column.

Structuring Your Invoice Data as JSON

In a production system, invoice data typically comes from a database or e-commerce API. For this demo, it comes from a JSON file:

[
  {
    "invoiceNum": 100,
    "accountName": "Customer Alpha",
    "accountNumber": 1,
    "paymentDueDate": "August 15, 2025",
    "lineItems": [
      { "product": "Product 1", "qty": 5, "price": 2, "totalPrice": 10 },
      { "product": "Product 5", "qty": 3, "price": 9, "totalPrice": 18 },
      { "product": "Product 4", "qty": 1, "price": 50, "totalPrice": 50 },
      { "product": "Product X", "qty": 2, "price": 15, "totalPrice": 30 }
    ]
  },
  {
    "invoiceNum": 25,
    "accountName": "Customer Beta",
    "accountNumber": 2,
    "paymentDueDate": "August 15, 2025",
    "lineItems": [
      { "product": "Product 2", "qty": 9, "price": 2, "totalPrice": 18 },
      { "product": "Product 4", "qty": 1, "price": 8, "totalPrice": 8 },
      { "product": "Product 3", "qty": 10, "price": 25, "totalPrice": 250 },
      { "product": "Product YY", "qty": 3, "price": 15, "totalPrice": 45 },
      { "product": "Product AA", "qty": 2, "price": 100, "totalPrice": 200 }
    ]
  },
  {
    "invoiceNum": 51,
    "accountName": "Customer Gamma",
    "accountNumber": 3,
    "paymentDueDate": "August 15, 2025",
    "lineItems": [
      { "product": "Product 9", "qty": 1, "price": 2, "totalPrice": 2 },
      { "product": "Product 23", "qty": 30, "price": 9, "totalPrice": 270 },
      { "product": "Product ZZ", "qty": 6, "price": 15, "totalPrice": 90 }
    ]
  }
]

The array holds three invoice objects, each matching the structure of the Word template. The accountNumber field has no matching template token, and that’s intentional. Your data can contain fields the template doesn’t use, and accountNumber is used in the Python script to build unique output filenames.

Calling the Document Generation API with Python

The Generate Document endpoint requires your credentials, a base64-encoded version of the template, and your data. The complete demo runs in just over 50 lines of Python:

import os
import requests
import sys
from time import sleep
import base64
import json
from datetime import datetime

CLIENT_ID = os.environ.get('CLIENT_ID')
CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
HOST = os.environ.get('HOST')

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()

with open('invoice.docx', 'rb') as file:
	bd = file.read()
	b64 = base64.b64encode(bd).decode('utf-8')

with open('invoicedata.json', 'r') as file:
	data = json.load(file)

for invoiceData in data:
	result = docGen(b64, invoiceData, CLIENT_ID, CLIENT_SECRET)

	if result["base64FileString"] == None:
		print("Something went wrong.")
		print(result)
		sys.exit()

	b64_bytes = result["base64FileString"].encode('ascii')
	binary_data = base64.b64decode(b64_bytes)

	filename = f"invoice_account_{invoiceData["accountNumber"]}.pdf"

	with open(filename, 'wb') as file:
		file.write(binary_data)
		print(f"Done and stored to {filename}")

After importing modules and loading credentials from environment variables, the docGen function takes the template, data, and credentials, then posts to the API endpoint. The API returns the rendered PDF as a base64 string.

The main loop reads and base64-encodes the template, loads the JSON file, iterates over each invoice object, calls the API, and writes the result to a uniquely named file using accountNumber. You don’t have to write results to disk at all, since the raw binary data can go straight to an email attachment or a document storage system.

Keep one limit in mind, since the GenerateDocumentBase64 endpoint rejects a .docx payload larger than 4 MB after base64 encoding. If your template approaches that ceiling, compress its images through Word’s Picture Format tools, drop embedded fonts and OLE objects, and split oversized templates into smaller ones.

Example PDF result

The rendered PDF invoice the script produces, with all tokens replaced by values from the JSON payload.

Generate Invoice PDF API FAQ

The API returns either a PDF or a DOCX file. You specify the format in the outputFormat field of the request body, as shown in the Python example above.

Yes. The documentValues parameter accepts any JSON-serializable object, so you can pull data from Salesforce, a SQL database, a REST endpoint, or any other source and pass it directly to the API. The JSON file in this tutorial is a stand-in for whatever data source you use in production.

No. You design templates in standard Microsoft Word. The API reads the token syntax ({{ }}) from any valid .docx file, with no Foxit desktop software or proprietary editor required.

Foxit’s API platform is SOC 2 Type II certified and supports GDPR and HIPAA compliance, which makes it suitable for invoices that include customer personally identifiable information or regulated financial data. See Foxit’s API security and compliance page for the full list of frameworks.

The free Developer plan gives you instant access. It covers this tutorial and experimentation with your own templates before committing to a paid tier.

Next Steps

Get free credentials and download the template, Python script, and sample output from the GitHub repository to run this demo yourself.

Two natural extensions follow from here. Foxit Connectors provides 40+ pre-built integrations with platforms like Salesforce, SharePoint, and Google Drive, so you can pull invoice data directly from your CRM or ERP rather than a local JSON file. You can also chain document generation with an eSign step, sending the invoice for client acknowledgment immediately after creation, by passing the generated PDF to the Foxit eSign API.

DOCX to PDF via the Foxit PDF Services API: Python and cURL Walkthrough

Foxit docx to pdf api four-step conversion flow in Python and cURL.

This walkthrough covers the full DOCX-to-PDF flow on the Foxit PDF Services API with runnable Python and cURL for each call.

Automated document pipelines demand conversion tooling that accepts a file, queues a job, and returns clear status at every step. The Foxit PDF Services API gives you exactly that: a four-endpoint async flow covering upload, convert, poll, and download. Each step returns a typed payload, the task model exposes four explicit states with a numeric progress field, and error codes map cleanly to distinct recovery paths.

This tutorial walks through every step of that flow in Python 3 with the requests library, plus cURL equivalents for each call. You’ll have a runnable convert.py script you can drop into a pipeline today.

Prerequisites

Before you run a single line of this tutorial, get the following in place. Each item links to its canonical install or setup guide.

  • Python 3.8 or newer — verify with python3 --version. The script uses only standard library modules plus one external package, so any modern 3.x will do.
  • pip — bundled with Python 3.4+. Verify with python3 -m pip --version.
  • A virtual environment — isolates project dependencies so they don’t collide with system Python or other projects. See the venv tutorial for platform-specific activation commands.
  • The requests library — the only third-party dependency in this walkthrough. Installed inside the venv below.
  • A code editor — Visual Studio Code with the Python extension is a solid default, but PyCharmSublime Text, or any editor you like will work.
  • cURL — pre-installed on macOS and most Linux distros. Windows users can install from the official site or use WSL.
  • A Foxit Developer account — register for free (no credit card required). The Foxit Developer Portal provisions a default application with your CLIENT_ID and CLIENT_SECRET immediately after signup.

Set up the project workspace:

mkdir foxit-docx-to-pdf && cd foxit-docx-to-pdf
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install requests

Export your credentials as environment variables so the script never sees them as hardcoded strings:

export CLIENT_ID=your_client_id_here
export CLIENT_SECRET=your_client_secret_here
Your Python script reads them with `os.environ.get()`:
import os

CLIENT_ID = os.environ.get("CLIENT_ID")
CLIENT_SECRET = os.environ.get("CLIENT_SECRET")

BASE_URL = "https://na1.fusion.foxit.com"

All four API calls go to https://na1.fusion.foxit.com. The developer portal also offers a live sandbox and pre-built Postman collections if you want to verify calls in a GUI before scripting.

For a sample DOCX to work with right away, download input.docx directly from the foxitsoftware/developerapidemos GitHub repository and save it to your working directory.

How the Auth Model Works

The Foxit PDF Services API authenticates through named request headers. Pass client_id and client_secret directly on every call:

headers = {
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
}
That single dict covers upload (multipart POST), polling (GET), and download (GET). The convert endpoint takes a JSON body, so it requires `Content-Type: application/json` as well:
json_headers = {
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
    "Content-Type": "application/json",
}

The API expects the raw key/secret pair in those named headers. Wrapping credentials in an Authorization: Bearer header instead returns 400, since the required client_id and client_secret headers are missing.

Step 1 and Step 2: Upload the DOCX and Initiate Conversion

Step 1: Upload the DOCX File

POST /pdf-services/api/documents/upload accepts the file as multipart/form-data and returns a documentId that every subsequent call needs.

import requests

def upload_doc(file_path: str) -> str:
    url = f"{BASE_URL}/pdf-services/api/documents/upload"
    headers = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    with open(file_path, "rb") as f:
        files = {"file": (os.path.basename(file_path), f)}
        response = requests.post(url, headers=headers, files=files)
    response.raise_for_status()
    return response.json()["documentId"]

cURL equivalent:

curl -X POST "https://na1.fusion.foxit.com/pdf-services/api/documents/upload" \
  -H "client_id: $CLIENT_ID" \
  -H "client_secret: $CLIENT_SECRET" \
  -F "[email protected]"

Uploaded files carry a 100 MB cap and are automatically deleted after 24 hours. A documentId scopes to the current upload session and expires with the source file, so treat it as ephemeral.

Step 2: Initiate the PDF Conversion

POST /pdf-services/api/documents/create/pdf-from-word accepts a JSON body with the documentId and returns a taskId. The API handles 10 to 10,000+ conversions per day across production pipelines, queuing jobs asynchronously to avoid blocking the connection until the PDF is ready.

import json

def convert_to_pdf(document_id: str) -> str:
    url = f"{BASE_URL}/pdf-services/api/documents/create/pdf-from-word"
    headers = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "Content-Type": "application/json",
    }
    payload = {"documentId": document_id}
    response = requests.post(url, headers=headers, data=json.dumps(payload))
    response.raise_for_status()
    return response.json()["taskId"]

cURL equivalent:

curl -X POST "https://na1.fusion.foxit.com/pdf-services/api/documents/create/pdf-from-word" \
  -H "client_id: $CLIENT_ID" \
  -H "client_secret: $CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"documentId": "<your_document_id>"}'

The endpoint returns 202 Accepted, confirming the job is queued. It also accepts .doc.rtf.dot.dotx.docm.dotm, and .wpd files through the same documentId input, so legacy Word formats work through the same pipeline.

Step 3: Polling the Task Status

GET /pdf-services/api/tasks/{task-id} returns four fields you need to act on in your polling loop:

  • status: one of PENDINGIN_PROGRESSCOMPLETED, or FAILED
  • progress: int32, 0 to 100
  • resultDocumentId: populated when status reaches COMPLETED
  • error: populated when status reaches FAILED

The task state machine advances in one direction: PENDING to IN_PROGRESS, then to either COMPLETED or FAILED.

Foxit DOCX to PDF API task state machine: PENDING to IN_PROGRESS, then COMPLETED with resultDocumentId or FAILED with an error object.

import time

def poll_task(task_id: str, max_attempts: int = 30) -> str:
    url = f"{BASE_URL}/pdf-services/api/tasks/{task_id}"
    headers = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    for attempt in range(max_attempts):
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        data = response.json()
        status = data.get("status")
        progress = data.get("progress", 0)
        print(f"Attempt {attempt + 1}: status={status}, progress={progress}%")
        if status == "COMPLETED":
            return data["resultDocumentId"]
        if status == "FAILED":
            raise RuntimeError(f"Conversion failed: {data.get('error')}")
        time.sleep(2)
    raise TimeoutError(f"Task {task_id} did not complete in {max_attempts} attempts")

Two-second polling intervals work across a wide range of document sizes, and polling more aggressively only consumes rate limit budget without affecting conversion time.

Step 4: Downloading the Converted PDF

GET /pdf-services/api/documents/{documentId}/download fetches the finished PDF. The path parameter in the API reference reads {documentId}, but the value you pass here is the resultDocumentId from the completed poll response. The server assigns that ID to the generated PDF output at conversion time, making it the correct identifier to use at this step.

Stream the response to disk with stream=True and iter_content(chunk_size=8192). Buffering a large PDF fully into memory before writing it causes problems on high-volume pipelines.

def download_result(result_document_id: str, output_path: str) -> None:
    url = f"{BASE_URL}/pdf-services/api/documents/{result_document_id}/download"
    headers = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    with requests.get(url, headers=headers, stream=True) as response:
        response.raise_for_status()
        with open(output_path, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)

The cURL equivalent uses the --output flag to write directly to disk:

curl -X GET "https://na1.fusion.foxit.com/pdf-services/api/documents/<result_document_id>/download" \
  -H "client_id: $CLIENT_ID" \
  -H "client_secret: $CLIENT_SECRET" \
  --output output.pdf

To verify the output, check response.headers.get("Content-Type") for application/pdf, or inspect the first four bytes of the written file for the %PDF magic bytes if your pipeline requires format validation.

Error Handling for Production

The Foxit PDF Services API documentation covers 400, 404, 413, and 500 across the four endpoints. The 401 appears on authentication failures as a practical case even though it’s absent from the documented example responses. Each status code points to a specific root cause with a concrete recovery path:

  • 400: malformed request body or unsupported file type. Validate the input file path and extension before calling upload_doc().
  • 401: credential misconfiguration. Verify that CLIENT_ID and CLIENT_SECRET are exported in your shell and that the header names are lowercase client_id and client_secret.
  • 404: the documentId has expired. The server deletes uploaded files after 24 hours, so the convert and download endpoints return 404 for any documentId past that window. Re-upload the source file and restart from the upload step. An expired or unknown taskId on the poll endpoint behaves differently: it returns HTTP 200 with status: "FAILED" and an error object whose message reads "task is not exist". The poll loop’s FAILED branch already catches that case.
  • 413: file exceeds the 100 MB upload cap. Pre-check with os.path.getsize() before uploading, or split the document.
  • 500: transient server error. Apply exponential backoff with a ceiling of 3 retries (wait times of 1s, 2s, and 4s).
def call_with_retry(fn, *args, max_retries: int = 3, **kwargs):
    for attempt in range(max_retries + 1):
        try:
            return fn(*args, **kwargs)
        except requests.HTTPError as e:
            code = e.response.status_code
            if code == 400:
                raise ValueError(
                    "Bad request. Confirm the input is a supported Word format."
                ) from e
            if code == 401:
                raise PermissionError(
                    "Authentication failed. Check CLIENT_ID and CLIENT_SECRET env vars."
                ) from e
            if code == 404:
                raise FileNotFoundError(
                    "Document or task expired (24h TTL). Re-upload and retry."
                ) from e
            if code == 413:
                raise OverflowError(
                    "File too large. The upload cap is 100 MB."
                ) from e
            if code == 500 and attempt < max_retries:
                wait = 2 ** attempt  # 1s, 2s, 4s
                print(f"Server error. Retrying in {wait}s ({attempt + 1}/{max_retries})")
                import time
                time.sleep(wait)
                continue
            raise

Pipeline authors should treat documentId values as ephemeral: each one expires with its source file after 24 hours, so pipeline code that caches documentId values between sessions will see 404s on every convert call, and re-uploading is always the correct recovery path.

The Complete Script

Set your environment variables, then run python convert.py input.docx output.pdf:

import os
import json
import time
import sys
import requests

CLIENT_ID = os.environ.get("CLIENT_ID")
CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
BASE_URL = "https://na1.fusion.foxit.com"


def call_with_retry(fn, *args, max_retries: int = 3, **kwargs):
    for attempt in range(max_retries + 1):
        try:
            return fn(*args, **kwargs)
        except requests.HTTPError as e:
            code = e.response.status_code
            if code == 400:
                raise ValueError(
                    "Bad request. Confirm the input is a supported Word format."
                ) from e
            if code == 401:
                raise PermissionError(
                    "Authentication failed. Check CLIENT_ID and CLIENT_SECRET env vars."
                ) from e
            if code == 404:
                raise FileNotFoundError(
                    "Document or task expired (24h TTL). Re-upload and retry."
                ) from e
            if code == 413:
                raise OverflowError(
                    "File too large. The upload cap is 100 MB."
                ) from e
            if code == 500 and attempt < max_retries:
                wait = 2 ** attempt  # 1s, 2s, 4s
                print(f"Server error. Retrying in {wait}s ({attempt + 1}/{max_retries})")
                time.sleep(wait)
                continue
            raise


def upload_doc(file_path: str) -> str:
    url = f"{BASE_URL}/pdf-services/api/documents/upload"
    headers = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}
    with open(file_path, "rb") as f:
        files = {"file": (os.path.basename(file_path), f)}
        r = requests.post(url, headers=headers, files=files)
    r.raise_for_status()
    return r.json()["documentId"]


def convert_to_pdf(document_id: str) -> str:
    url = f"{BASE_URL}/pdf-services/api/documents/create/pdf-from-word"
    headers = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "Content-Type": "application/json",
    }
    r = requests.post(url, headers=headers, data=json.dumps({"documentId": document_id}))
    r.raise_for_status()
    return r.json()["taskId"]


def poll_task(task_id: str, max_attempts: int = 30) -> str:
    url = f"{BASE_URL}/pdf-services/api/tasks/{task_id}"
    headers = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}
    for attempt in range(max_attempts):
        r = requests.get(url, headers=headers)
        r.raise_for_status()
        data = r.json()
        status = data.get("status")
        print(f"[{attempt + 1}/{max_attempts}] status={status}, progress={data.get('progress', 0)}%")
        if status == "COMPLETED":
            return data["resultDocumentId"]
        if status == "FAILED":
            raise RuntimeError(f"Conversion failed: {data.get('error')}")
        time.sleep(2)
    raise TimeoutError(f"Task {task_id} did not complete after {max_attempts} attempts")


def download_result(result_document_id: str, output_path: str) -> None:
    url = f"{BASE_URL}/pdf-services/api/documents/{result_document_id}/download"
    headers = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}
    with requests.get(url, headers=headers, stream=True) as r:
        r.raise_for_status()
        with open(output_path, "wb") as f:
            for chunk in r.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)


def convert_docx_to_pdf(input_path: str, output_path: str) -> None:
    print(f"Uploading {input_path}...")
    document_id = call_with_retry(upload_doc, input_path)
    print(f"Uploaded. documentId={document_id}")

    print("Initiating conversion...")
    task_id = call_with_retry(convert_to_pdf, document_id)
    print(f"Queued. taskId={task_id}")

    print("Polling for completion...")
    result_document_id = call_with_retry(poll_task, task_id)
    print(f"Completed. resultDocumentId={result_document_id}")

    print(f"Downloading to {output_path}...")
    call_with_retry(download_result, result_document_id, output_path)
    print("Done.")


if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python convert.py <input.docx> <output.pdf>")
        sys.exit(1)
    convert_docx_to_pdf(sys.argv[1], sys.argv[2])

The Foxit PDF Services API also supports merging, compression, linearization, and OCR through additional endpoints. All of them share the same host and header-based auth pattern, so the functions you’ve built here extend naturally as your pipeline grows.

Create your free Foxit developer account and run your first conversion in under five minutes, with no credit card required at signup.

DOCX to PDF API FAQ

Yes. The /pdf-services/api/documents/create/pdf-from-word endpoint accepts .doc, .docx, .rtf, .dot, .dotx, .docm, .dotm, and .wpd. The same four-step flow applies for all of them.

Uploaded documents are automatically deleted after 24 hours. Treat documentId values as ephemeral and re-upload whenever you need to convert a file after that window.

Faster polling consumes rate limit budget without affecting conversion speed. The server determines conversion time based on document complexity and queue load, so polling intervals below 2 seconds add no throughput benefit.

Yes. Each upload returns an independent documentId and each conversion returns an independent taskId. Run concurrent conversions by launching multiple threads or async tasks, with each one tracking its own taskId. Python’s concurrent.futures.ThreadPoolExecutor is a straightforward way to manage this.

From the Foxit Developer Portal dashboard, under the default application created at signup. Both values are available immediately after account creation.

The API authenticates through named request headers. Pass client_id and client_secret directly on every request, and the server reads those credentials on each call.

Document Generation Explained: How the Template-to-API Pipeline Actually Works

Document generation pipeline showing a Word template and JSON payload merging through the Foxit DocGen API into a finished PDF.

Manual document workflows break down fast as volume grows. This guide explains what document generation is, how template-driven APIs replace manual processes, and what the pipeline looks like from a Word template and JSON payload to a finished PDF.

You’ve inherited a document workflow built on Word macros, save-as duplication, and a shared drive folder someone named “FINAL_v3.” Every time a contract needs to go out, someone opens the master template, manually replaces the client name and date, exports to PDF, and emails it. Scaled to one deal a week, that works. Scaled to a thousand deals a quarter, it breaks down in ways that are hard to trace and painful to fix.

Document generation APIs make the relationship between input data and output document deterministic. This article covers how that pipeline is structured, what the token contract between template and data looks like, and what the actual API call looks like from a POST request to a decoded file.

What Document Generation Is

Document generation is the programmatic production of populated, formatted documents from a template and a structured data source. Three components are always present: a template (the structure and placeholders), a data payload (the values to inject), and a rendering engine (the API that merges the two and produces the final file).

These components map cleanly onto a separation of concerns. The template owner controls layout, language, and branding. The data owner controls what goes in each field. The rendering engine enforces the merge contract between them. When that separation holds, changing the template doesn’t require a code change, and changing the data schema requires only a template update.

Document generation occupies a distinct category from the adjacent tools that crowd the same search space. E-signature platforms collect binding signatures on completed documents. Document management systems store, version, and retrieve files. OCR and extraction tools pull structured data out of existing documents. Document generation puts data in.

Why Manual Document Workflows Break Under Load

Manual Word-based workflows fail in three predictable ways when volume grows.

Version drift happens when templates live in shared folders across teams. One team updates the disclaimer text, another adds a new liability clause, and a third is still using a version from Q3. After six months, your organization has five variants of the same contract template producing inconsistent output, with no reliable way to identify which version generated any given document.

Merge errors compound at scale. Copy-paste and mail-merge workflows both require human coordination of field-by-field substitution. When an invoice ships with the previous client’s name, or a renewal letter shows last year’s rates, the error traces back to a single manual step that had no validation layer. A 0.5% error rate is invisible at 20 documents a month. At 4,000 documents a month, it means 20 wrong documents going out the door.

Audit trail absence creates compliance exposure. A manually assembled document carries no machine-readable record of what data produced it or when. When a regulator asks which policy documents were generated from the October 2024 rate table, the answer requires a manual search through email threads and file name timestamps.

Programmatic generation makes each of these problems tractable. The same JSON payload processed against the same template version always produces the same output, and every generation event is a logged API call with a traceable input and output. The document generation software market is valued at $4.05B in 2025 and projected to grow at a 9.2% CAGR through 2035, driven by enterprise automation and compliance requirements that manual workflows can’t satisfy at scale.

How Template-Driven Generation Works: Tokens, Loops, and Conditionals

Foxit’s DocGen API uses standard Microsoft Word as the template authoring environment. Template authors work in Word, inserting double-bracket placeholders whose key names map directly to keys in the JSON payload supplied at generation time. This keeps template ownership with the people who understand the document content, with no proprietary editor and no additional software license required.

The basic token syntax covers three common cases:

  • {{ companyName }} renders the string value of companyName from the payload
  • {{ invoiceDate \@ MM/dd/yyyy }} applies a Word date picture string to the raw date value (the leading \@ is required; the friendly form without it renders blank)
  • {{ totalDue \# "$#,##0.00" }} formats a numeric value as currency using a Word numeric picture string (a friendly keyword like \# Currency is unsupported and renders blank)

A minimal JSON payload for a template containing those three tokens would be:

{
  "companyName": "Meridian Analytics",
  "invoiceDate": "2025-06-15",
  "totalDue": 8250.0
}

The rendering engine walks the template, matches each token to the corresponding key in the payload, and writes the formatted value into the output. Template authors work entirely in Word, while the engine handles token resolution, format application, and output assembly.

Repeating sections use loop delimiters to produce table rows that repeat for each element in a JSON array. Placing {{TableStart:lineItems}} before a table row and {{TableEnd:lineItems}} after it tells the engine to emit one row per object in the lineItems array. Both delimiters must sit in cells of the same Word table row. Inside that loop, {{ROW_NUMBER}} auto-increments across rows, and a footer row immediately below the loop can use {{=SUM(ABOVE) \# "$#,##0.00"}} to compute and format a column total, so a ten-line invoice produces a correctly numbered, fully summed table with no post-processing.

Conditional content uses Word’s native Field Code View (opened with ALT + F9) to write IF-field conditions that show or hide text blocks based on data values. A clause that should appear only when contractType equals "enterprise" lives inside a field condition, and the rendering engine evaluates it at generation time. There’s no separate scripting layer and no custom expression language to learn.

The three components converge at a single API endpoint: your base64-encoded DOCX template and JSON data payload go in together, and the generated document comes back in the same HTTP response.

Document generation pipeline diagram showing a Word template with tokens and a JSON data payload feeding a POST to GenerateDocumentBase64, which passes through the Foxit DocGen rendering engine and returns a synchronous JSON response decoded into a final PDF or DOCX file.

The API Pipeline: From POST Request to Final Document

The Foxit DocGen API compresses the generation pipeline into a single synchronous call. You POST to one endpoint with your template and data, and you receive the generated document in the same HTTP response, with no separate template upload step, no job ID to poll, and no webhook to configure for individual document generation.

Before building a generation workflow, you can use the Analyze Document API to scan a DOCX template and return a list of all embedded tokens, which confirms the token-to-key mapping before you commit to a data schema. That’s a single POST to a separate endpoint on the same host, and it returns a structured list of placeholder names and their types.

For the generation call itself, the dev-tier endpoint is:

POST https://na1.fusion.foxit.com/document-generation/api/GenerateDocumentBase64

Authentication passes your client_id and client_secret as custom HTTP headers alongside Content-Type: application/json. You retrieve both credentials from the dashboard at account.foxit.com/site/sign-up after activating your free developer plan, with no OAuth exchange and no session setup required before your first call.

The request body takes three fields:

  • base64FileString is your DOCX template, base64-encoded. Keep the source .docx under 4 MB (the practical ceiling for a single request) since base64 encoding inflates the payload by roughly 33%. If a template runs large, embedded images are usually the cause, so compress them through Word’s Picture Format settings before exporting.
  • documentValues is the JSON object whose keys map to token names in the template
  • outputFormat is the string "pdf" or "docx", lowercase and exact (the API returns HTTP 500 for any other value, including "PDF" or "DOCX")

For an invoice template with a lineItems array, the full curl command carries your base64-encoded DOCX, the matching data object, and your credentials in the request headers:

curl -X POST "https://na1.fusion.foxit.com/document-generation/api/GenerateDocumentBase64" \
  -H "client_id: YOUR_CLIENT_ID" \
  -H "client_secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "base64FileString": "BASE64_ENCODED_DOCX_HERE",
    "documentValues": {
      "companyName": "Meridian Analytics",
      "invoiceDate": "2025-06-15",
      "totalDue": 8250.00,
      "lineItems": [
        { "description": "Platform License", "quantity": 1, "unitPrice": 8250.00 }
      ]
    },
    "outputFormat": "pdf"
  }'

The API returns a synchronous JSON response carrying a human-readable message, the fileExtension for naming the output file, and the  base64FileString containing the generated document:

{
  "message": "PDF Document Generated Successfully",
  "fileExtension": "pdf",
  "base64FileString": "JVBERi0xLjQK..."
}

message gives you a status string for logging. fileExtension tells you whether you received "pdf" or "docx", which lets you construct the output filename programmatically without parsing the message string. base64FileString is the generated document. Your application decodes that value and routes the resulting bytes to storage, email delivery, a document management system, or whatever downstream step your workflow requires.

For teams evaluating the API before writing integration code, the Foxit developer portal includes a Postman collection with GenerateDocumentBase64 preconfigured. You can load the collection, paste your credentials and a test template, send the request, and confirm the response structure before writing a single line of application code. Foxit also provides SDKs for Node.js, Python, Java, C#, and PHP if you prefer a language-native integration path over raw HTTP.

Where Organizations Are Actually Using This

Five industries where the template-to-API pipeline produces direct operational value:

Insurance carriers face renewal cycles that generate thousands of policy documents each quarter. They pull policyholder data from their CRM, pass it as a JSON payload to the generation API, and produce populated renewal packets without manual assembly. Each document reflects current policy terms from the system of record, cutting the per-document preparation time from minutes of human work to milliseconds of API latency.

Healthcare providers need patient intake packets and HIPAA disclosure forms ready before each appointment. Clinics pull patient demographic and consent data from their EHR system, generate the packet at appointment scheduling time, and deliver it to the patient portal. The data source is the live EHR record, so the document always reflects current information.

Government agencies and courts produce case-related documents (orders, notices, motions) with fixed structure and variable case data. API-based generation means each document draws directly from structured court records, reducing transcription errors and producing a machine-readable audit trail that links every output document to the specific data that created it.

High-tech and SaaS companies trigger NDA and quote generation directly from their CRM or CPQ tools. A deal record in Salesforce or HubSpot becomes the JSON payload, and the generation API produces a finalized, formatted document without a manual drafting step. The document lands in the deal room minutes after the pricing conversation closes.

Education institutions generate hundreds or thousands of admission offer letters during enrollment periods. Student data from the Student Information System becomes the payload, and each letter reflects the correct program, scholarship amount, and enrollment deadline for that individual student. What a staff member would take days to produce manually runs as a scheduled batch job.

What to Evaluate When Choosing a Document Generation API

Output format coverage deserves attention before you commit to an API. Receiving both PDF and DOCX from the same endpoint matters when your workflow requires human review before a document is finalized. PDF suits direct delivery. DOCX suits draft-and-review cycles where a lawyer or editor needs to modify the generated document before it goes to signature. APIs that produce only PDF force you to finalize at generation time, which eliminates the review step entirely.

The execution model determines whether an API fits your latency requirements. Synchronous APIs return the document in the same HTTP response, which works for real-time generation triggered by a user action or a CRM event. Asynchronous APIs accept a job and require polling or a webhook to retrieve the result, which works better for batch jobs processing thousands of documents in a run. Confirm which model the API offers before you design your integration, because retrofitting from synchronous to async (or vice versa) affects how you handle errors, retries, and downstream routing. Foxit’s DocGen API is synchronous, so individual requests resolve in a single HTTP round trip.

Template portability determines your long-term maintenance cost. A template stored as a standard DOCX file is editable by anyone with Word, version-controllable in Git, and portable across environments. A template stored in a proprietary format requires the vendor’s editor for every update, and losing access to that editor means losing the ability to maintain your own document logic. Word-based templates also let business users own content changes without involving a developer.

Compliance posture matters as soon as your documents contain PII, PHI, or financial data. Confirm the provider’s certifications before sending real records through the API. SOC 2, GDPR compliance, and HIPAA certification are the relevant checks for most enterprise document workflows. A provider’s architecture (multi-tenant SaaS vs. single-tenant hosted) also affects how you address data residency requirements.

Developer onboarding cost is a real selection criterion. A free tier with immediate credential access, a working Postman collection, and SDK support for your language stack lets your team validate fit in hours. A procurement process that requires a sales conversation before you can run a test extends your evaluation cycle by weeks. Foxit’s developer plan is free, credit-card-free, and gives you dashboard access and credentials immediately, so your team can make an informed build-vs-buy decision based on a working integration rather than a demo.

Getting Started: Generate Your First Document Today

A working end-to-end generation pipeline takes under an hour to set up:

  1. Go to account.foxit.com/site/sign-up and activate a free Developer plan. Dashboard access and credentials are immediate, with no credit card and no sales call required.

  2. Retrieve your Client ID and Client Secret from the dashboard.

  3. Grab a ready-to-use template, or author your own. Download invoice_simple.docx for the smallest possible smoke test, or invoice_table.docx if you want the full loop, {{ROW_NUMBER}}, and {{=SUM(ABOVE)}} round-trip. To build your own instead, open Microsoft Word, add two or three {{ tokenName }} placeholders, and save as DOCX. Base64-encode the file using base64 -i template.docx on macOS or Linux, or [Convert]::ToBase64String([IO.File]::ReadAllBytes("template.docx")) in Windows PowerShell.

  4. Open the Postman collection linked on the Foxit API page, load the GenerateDocumentBase64 request, paste your base64-encoded template into base64FileString, and add a JSON object to documentValues with keys matching your placeholder names. Set outputFormat to "pdf" and send the request. Copy the base64FileString value from the JSON response and decode it. You now have a generated PDF.

From this point, connecting to a real data source is the only remaining step. Pull a record from your CRM, a row from your database, or a response from an upstream API, map its fields to the token names in your template, and pass the result as documentValues. That connection makes the pipeline production-ready, and every document it generates becomes a deterministic, auditable function of the data that produced it.

Activate your free Foxit Developer plan (no credit card, no sales call) and run your first document generation call in minutes at account.foxit.com/site/sign-up.

Document Generation Pipeline FAQ

Document generation is the programmatic production of populated, formatted documents from a template plus a structured data source. Three components are always present: a template holding the layout and placeholders, a JSON data payload holding the values, and a rendering engine that merges the two into a final PDF or DOCX. The same payload against the same template always produces the same output, which makes the process deterministic and auditable.

They occupy adjacent but distinct categories. E-signature platforms like DocuSign collect binding signatures on documents that already exist. Document management systems store, version, and retrieve files. OCR and extraction tools pull structured data out of existing documents. Document generation does the opposite — it puts data in, producing a new populated document from a template. Many workflows chain them: generate a contract, then route it for signature.

You POST to GenerateDocumentBase64 with three fields: base64FileString (your base64-encoded DOCX template), documentValues (the JSON object whose keys match your tokens), and outputFormat ("pdf" or "docx", lowercase). Authentication uses client_id and client_secret headers. The synchronous response returns a message, a fileExtension, and a base64FileString containing the generated document, which your app decodes and routes downstream.

Use loop delimiters. Place {{TableStart:lineItems}} before a Word table row and {{TableEnd:lineItems}} after it, with both delimiters in cells of the same row. The engine emits one row per object in the lineItems array. Inside the loop, {{ROW_NUMBER}} auto-increments, and a footer row can use {{=SUM(ABOVE) \# "$#,##0.00"}} to compute and format a column total — so a ten-line invoice renders fully numbered and summed with no post-processing.

Yes. The Foxit DocGen API returns either format from the same endpoint based on the outputFormat value. PDF suits direct delivery where the document is final at generation time. DOCX suits draft-and-review cycles, where a lawyer or editor needs to modify the generated file before it goes to signature. APIs that emit PDF only force you to finalize at generation and eliminate that review step.

PDF Translation with Verifiable Quality: Build a Confidence-Scored Pipeline with Foxit API and Straker.ai

Architecture diagram of a PDF translation API pipeline using Foxit and Straker.ai with per-segment confidence scoring.

Most machine translation tools hand back a translated PDF with no signal about which parts to trust — a real problem for contracts, medical forms, and regulatory filings. This guide shows how to build a pipeline that scores every segment before the final render, using Foxit for structural extraction and layout-preserving rendering and Straker.ai for translation plus per-segment quality scoring.

Most machine translation tools give you a translated file and nothing else. They do not tell you which parts are correct and which parts are wrong. For a simple blog post, that is fine. For a contract, a medical form, or a legal notice, it is a real problem. A bad translation can sit in the final PDF for days before anyone notices, often only after the document has already been signed or sent.

Teams today are translating more documents, into more languages, and faster than ever. Legal, finance, healthcare, HR, and insurance teams all deal with PDFs where one wrong word can cause a lot of damage: a broken contract, a failed audit, or even a safety issue. Most translation tools were not built to catch these mistakes. They just move text from one language to another. When quality checks happen at all, they usually mean a person reading the final PDF line by line and hoping they spot the errors.

This article shows how to build a better setup. You will learn how to build a PDF translation pipeline that gives every segment a quality score before the final PDF is created. Instead of hoping the translation is right, the pipeline tells you which parts to trust, which parts to review, and which parts to send back to a human translator. All of this happens automatically on every run.

Architecture at a Glance

Before going deeper, it helps to see the full pipeline in one picture. The diagram below traces a source PDF through every stage: extract, translate, score, route, and render. Each box is a single responsibility handled by a single service, with the routing layer acting as the glue you control.

High-level PDF translation API architecture showing source PDF flowing through Foxit structural extract, Straker AI translate and score, routing layer for accept/flag/reject, Foxit layout-preserving render, and final translated PDF.

The pipeline has two external services:

  • Foxit PDF Translation API handles anything PDF-specific. It pulls the structured text out of the source document with element IDs attached, then renders the final PDF back in the original layout (multi-column text, tables, font substitution, image positions) using the approved translations.
  • Straker AI translates each source segment AND scores the translation in the same request. It returns the target text, a numeric score on a 0.0 to 1.0 scale, and a categorical label (bestgoodacceptablebad) for every element ID. This step is pluggable, so you can swap Straker for DeepLGoogle Cloud TranslationAWS Translate, or an in-house NMT if you already have a contract with one of them. The contract between this step and the rest of the pipeline is a flat dict of element IDs to translated text plus per-segment scores.

and one piece of code you own:

  • Routing layer is your business logic. It reads the score, decides whether the segment auto-accepts, flags for human review, or escalates to a translator, and then hands the approved set to Foxit’s render call.

With the shape of the pipeline on the table, the rest of the article works through each piece in order, starting with why per-segment quality scoring is worth the integration effort in the first place.

The Quality Gap

You ship a translated PDF to a legal team. Three days later, compliance flags a clause in the German version. The term “indemnification” was rendered as “Entschädigung” (compensation) rather than “Freistellung” (hold harmless). Your MT pipeline returned a 200 status. Nobody’s alerting on that delta.

Raw machine translation output carries no quality signal by default. Every segment comes back translated, and your pipeline treats them identically regardless of whether the model was confident or guessing. For marketing copy that’s an acceptable tradeoff, but for a loan covenant, a clinical trial protocol, or a regulatory filing, a 95%-accurate translation can still be contractually or legally dangerous because the 5% failure may concentrate precisely in the high-stakes clauses.

A confidence score, in the translation QA context, is a per-segment numeric signal from a verification engine. It tells you how reliable each translated unit is on a scale your system can act on programmatically. High-confidence segments auto-accept, medium-confidence ones queue for post-edit review, and low-confidence segments escalate directly to a human translator before they ever reach the final document.

The compound problem for PDFs specifically is that most translation pipelines strip document structure before the MT engine even sees the text. The extraction step flattens multi-column layouts, collapses table cells, and drops font metadata. By the time you get a translated output, you’ve lost both layout fidelity and any quality signal. The rendered PDF looks wrong and you have no programmatic way to know which segments caused it.

Foxit’s PDF Translation Trial API extracts structured text from a source PDF with element IDs preserved, so the layout blueprint travels alongside the text through the entire workflow. You hand the source segments to Straker AI, which returns the translated text plus a per-segment numeric score and a quality label in a single call. (If you already run DeepL, Google Cloud Translation, AWS Translate, or an in-house NMT Engine, you can drop it in at this step without changing the rest of the pipeline.) Your routing logic decides which segments pass, which get flagged, and which escalate to human review. Foxit’s render endpoint then re-assembles the PDF in the original layout using the accepted translations, giving you a layout-preserved translated PDF with a documentable quality trail attached to every segment.

How the Pipeline Works

Foxit and Straker are two independent APIs that you wire together. Foxit owns PDF structure, extracting structured text keyed by element ID and re-rendering the final PDF in the original layout. Straker AI handles translation and per-segment quality scoring in a single request, returning the translated text alongside a numeric score and a quality label. You own the routing decision that sits between the scores and the render call.

The pipeline runs in seven steps:

Seven-step PDF translation API pipeline: upload source PDF, structural extract, preprocess, translate and score with Straker AI, route by score, render, and download the translated PDF.

Foxit covers steps 1-3 and 6-7 (PDF structure and rendering). Straker AI covers step 4, producing translations and per-segment quality scores in one round-trip. Step 5 is your business logic.

The Foxit PDF Translation API defines steps 2, 3, and 6. The upload and download calls use the general PDF Services endpoints. Straker AI is a separate API at https://api-verify.straker.ai. You submit XLF 1.2 files containing source segments and Straker returns the translated target_text per segment plus a numeric score (0.0 to 1.0) and a quality label (bestgoodacceptablebad). Because Foxit’s ExtractedText.json is a flat { "elementId": "text" } map, and XLF trans-unit IDs round-trip through Straker’s external_id field unchanged, the element IDs Foxit emits are the same IDs that come back with translations and scores attached. That alignment is what makes programmatic routing possible.

One clarification for readers who’ve seen the Foxit-Straker partnership announcement: that partnership covers Foxit eSignature Services, enabling end users to translate and sign documents in the eSign product. That’s an end-user feature. The PDF Translation Trial API used here is a separate developer surface. Its OpenAPI spec (v2.2.0) contains zero Straker references, and the preprocess-pdf documentation explicitly instructs developers to “translate the text in ExtractedText.json using your preferred translation tool.” You wire the two APIs together manually. This tutorial uses Straker AI as the default translation engine because it produces translations and quality scores in the same call, but you can substitute DeepL, Google Cloud Translation, AWS Translate, or your own NMT at step 4 without changing the Foxit calls.

Credentials and Setup

Get your Foxit credentials at app.developer-api.foxit.com/pricing. The free Developer plan gives you 20 AI credits per month with no credit card and no sales call required. Once you’ve signed in, your Client ID and Client Secret appear in the developer dashboard. Every Foxit API call requires both in the request headers as client_id and client_secret (lowercase snake_case). Export them in your shell as FOXIT_CLIENT_ID and FOXIT_CLIENT_SECRET so the code below reads them from the environment rather than hard-coding secrets.

For Straker, sign up at straker.ai/ai-platform/verify for API access. Straker issues a UUID-style API token that you send as a bearer token on every call (Authorization: Bearer <your-token>). The API lives at https://api-verify.straker.ai and its full reference is published at api-verify.straker.ai/docs. Export your token as STRAKER_API_KEY for the code below. You can confirm the token works and check your balance with a quick GET /user/balance. Both services offer trial access, so you can build and test the full pipeline before any procurement conversation.

Before you finalize your language matrix, check both APIs for supported languages. Foxit’s render endpoint accepts 23 target language codes (enzhzh_twfrdeesitptnljakothvihiruartrplsvnonbda, and fi). Straker AI identifies languages by UUID rather than ISO code. You fetch the full list with GET /languages and look up the UUID for your target (for example, 917FF728-0725-A033-1278-33025F49CA40 is French (France), 917FF7D8-9107-0BF8-97EE-065C20F453DE is German). The intersection of the two sets determines your production language coverage.

If you already have a contract with DeepL, Google Cloud Translation, AWS Translate, or an in-house NMT service, you can swap that engine in at step 4. The pipeline contract upstream (Foxit element IDs mapped to source strings) and downstream (a dict of {element_id: {score, quality, target_text}} feeding the router) does not change. The code below uses Straker AI by default because the same API returns the translation and the quality signal in one call.

Building the PDF Translation Pipeline

The complete seven-step pipeline runs in Python using requestsjsonzipfileos, and the standard-library xml.etree.ElementTree for building XLF. The first snippet covers Foxit steps 1-3 (upload, structural extraction, and preprocessing).

import requests
import json
import zipfile
import io
import time

FOXIT_BASE = "https://na1.fusion.foxit.com/pdf-services/api"
HEADERS = {
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
}

def poll_task(task_id: str) -> dict:
    """Poll GET /tasks/{task_id} until COMPLETED or FAILED."""
    while True:
        r = requests.get(f"{FOXIT_BASE}/tasks/{task_id}", headers=HEADERS)
        r.raise_for_status()
        data = r.json()
        status = data.get("status")
        if status == "COMPLETED":
            return data
        if status == "FAILED":
            raise RuntimeError(f"Task {task_id} failed: {data.get('error')}")
        # PENDING or IN_PROGRESS: wait and retry
        time.sleep(3)

# Step 1: Upload source PDF
with open("source.pdf", "rb") as f:
    upload_resp = requests.post(
        f"{FOXIT_BASE}/documents/upload",
        headers=HEADERS,
        files={"file": ("source.pdf", f, "application/pdf")}
    )
upload_resp.raise_for_status()
source_document_id = upload_resp.json()["documentId"]

# Step 2: Structural Extract (async - must complete before preprocess)
extract_resp = requests.post(
    f"{FOXIT_BASE}/documents/pdf-structural-extract",
    headers=HEADERS,
    json={"documentId": source_document_id}
)
extract_resp.raise_for_status()  # 202 Accepted
extract_task_id = extract_resp.json()["taskId"]

extract_result = poll_task(extract_task_id)
extracted_doc_id = extract_result["resultDocumentId"]

# Step 3: Preprocess (synchronous - returns 200, no polling needed)
preprocess_resp = requests.post(
    f"{FOXIT_BASE}/documents/translation/preprocess-pdf",
    headers=HEADERS,
    json={"documentId": extracted_doc_id}
)

# Errors from preprocess-pdf per the Foxit spec:
#   400 VALIDATION_ERROR      - "Document ID is required"
#   500 INTERNAL_SERVER_ERROR - "Failed to preprocess document"
preprocess_resp.raise_for_status()
preprocess_result_id = preprocess_resp.json()["resultDocumentId"]

# Download the ZIP containing ExtractedText.json and StructureInfo.json
zip_resp = requests.get(
    f"{FOXIT_BASE}/documents/{preprocess_result_id}/download",
    headers=HEADERS
)
zip_resp.raise_for_status()

with zipfile.ZipFile(io.BytesIO(zip_resp.content)) as zf:
    extracted_text = json.loads(zf.read("ExtractedText.json"))
    # StructureInfo.json: do not modify - the render step requires it untouched
    # structure_info = json.loads(zf.read("StructureInfo.json"))

# extracted_text is now {"elementId1": "original text", "elementId2": "original text", ...}

The preprocess step is synchronous, which means you get a 200 OK directly with the resultDocumentId. No polling required. The ZIP it produces contains two files: ExtractedText.json maps every element ID to its original text, and StructureInfo.json carries the full layout blueprint (bounding boxes, font metadata, column positions). You pass StructureInfo.json to the render step unmodified. Modifying it breaks the render because it’s the mechanism that makes layout preservation possible.

The second snippet covers steps 4-7, calling Straker AI to translate and score every segment in one round-trip, routing by score, rendering the translated PDF, and downloading the result. Straker’s AI Translation and Quality Evaluation workflow accepts a source-only XLF and returns a translated target_text per segment alongside the numeric score and the quality label, so the same response feeds both the translation choice and the routing decision.

import xml.etree.ElementTree as ET

STRAKER_BASE = "https://api-verify.straker.ai"
STRAKER_TOKEN = "STRAKER_API_KEY"
STRAKER_HEADERS = {"Authorization": f"Bearer {STRAKER_TOKEN}"}

# Straker identifies languages by UUID. Look these up once via GET /languages
# and cache them. Full list: https://api-verify.straker.ai/languages
STRAKER_LANG_FRENCH = "917FF728-0725-A033-1278-33025F49CA40"
STRAKER_LANG_GERMAN = "917FF7D8-9107-0BF8-97EE-065C20F453DE"

# Workflow UUID for "AI Translation and Quality Evaluation". Fetch the full
# list of workflows once via GET /workflow and cache the UUID for the one you
# want; this workflow produces both the translation and the per-segment score.
STRAKER_WORKFLOW_AI_TRANSLATE_AND_EVAL = "390b47a9-d5dc-46ae-92e2-56c43d128c44"


def build_xlf_1_2_source_only(source_lang: str, target_lang: str,
                              sources: dict) -> bytes:
    """
    Build a minimal XLF 1.2 document with source segments and empty targets.
    trans-unit/@id preserves Foxit's element IDs; Straker surfaces the same
    value as `external_id` on the segments it returns, so the keys round-trip.
    """
    ns = "urn:oasis:names:tc:xliff:document:1.2"
    ET.register_namespace("", ns)
    xliff = ET.Element(f"{{{ns}}}xliff", {"version": "1.2"})
    file_el = ET.SubElement(xliff, f"{{{ns}}}file", {
        "source-language": source_lang,
        "target-language": target_lang,
        "datatype": "plaintext",
        "original": "foxit-extract",
    })
    body = ET.SubElement(file_el, f"{{{ns}}}body")
    for element_id, source_text in sources.items():
        unit = ET.SubElement(body, f"{{{ns}}}trans-unit", {"id": element_id})
        ET.SubElement(unit, f"{{{ns}}}source").text = source_text
        ET.SubElement(unit, f"{{{ns}}}target")  # empty - Straker fills it in
    return b'<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(xliff, encoding="utf-8")


# Step 4: Translate and score every segment with Straker AI in one call.
def translate_and_score_with_straker(sources: dict, source_lang_code: str,
                                     target_lang_uuid: str) -> dict:
    """
    Submit source-only XLF to Straker's AI Translation + Quality Evaluation
    workflow. Returns a dict keyed by Foxit element ID ->
    {"score": float|None, "quality": str, "target_text": str}.
    """
    xlf_bytes = build_xlf_1_2_source_only(source_lang_code, "fr", sources)

    # 4a. Create the project on the AI Translation + Quality Evaluation
    # workflow. confirmation_required=false commits the token cost
    # immediately; set to true to review cost and call POST /project/confirm
    # before processing begins.
    create_resp = requests.post(
        f"{STRAKER_BASE}/project",
        headers=STRAKER_HEADERS,
        files={"files": ("segments.xlf", xlf_bytes, "application/xliff+xml")},
        data={
            "languages": target_lang_uuid,
            "title": "Foxit PDF translation batch",
            "workflow_id": STRAKER_WORKFLOW_AI_TRANSLATE_AND_EVAL,
            "confirmation_required": "false",
        },
    )
    create_resp.raise_for_status()
    project_id = create_resp.json()["project_id"]

    # 4b. Poll the project until it reports COMPLETED.
    while True:
        status_resp = requests.get(
            f"{STRAKER_BASE}/project/{project_id}", headers=STRAKER_HEADERS
        )
        status_resp.raise_for_status()
        project = status_resp.json()["data"]
        if project["status"] == "COMPLETED":
            break
        if project["status"] in ("FAILED", "PROCESSING_FAILED", "CANCELED"):
            raise RuntimeError(f"Straker project {project_id} failed")
        time.sleep(3)

    # 4c. Fetch the per-segment translations + scores. file_uuid is returned
    # in the project payload.
    file_uuid = project["source_files"][0]["file_uuid"]
    seg_resp = requests.get(
        f"{STRAKER_BASE}/project/{project_id}/segments/{file_uuid}/{target_lang_uuid}",
        headers=STRAKER_HEADERS,
    )
    seg_resp.raise_for_status()

    results = {}
    for seg in seg_resp.json()["segments"]:
        element_id = seg["external_id"]  # matches the Foxit key we packed into XLF
        t = seg["translation"]
        results[element_id] = {
            "score": t["score"],          # float 0.0 to 1.0, or None
            "quality": t["quality"],      # "best" | "good" | "acceptable" | "bad"
            "target_text": t["target_text"],  # Straker's translation
        }
    return results

scored = translate_and_score_with_straker(
    extracted_text,
    source_lang_code="en",
    target_lang_uuid=STRAKER_LANG_FRENCH,
)

# Step 5: Route by score and quality label (developer-controlled business logic).
HIGH_THRESHOLD = 0.85
LOW_THRESHOLD = 0.65

accepted = {}
flagged_for_review = {}
rejected = {}

for element_id, verdict in scored.items():
    score = verdict["score"] or 0.0
    if verdict["quality"] == "best" or score >= HIGH_THRESHOLD:
        accepted[element_id] = verdict["target_text"]
    elif verdict["quality"] == "bad" or score < LOW_THRESHOLD:
        rejected[element_id] = {"original": extracted_text[element_id],
                                 "score": score, "quality": verdict["quality"]}
    else:
        flagged_for_review[element_id] = {"translation": verdict["target_text"],
                                           "score": score, "quality": verdict["quality"]}

# Build the render payload. Foxit's render expects every key from the original
# ExtractedText.json. Accepted segments use the scored translation; flagged and
# rejected segments fall back to the original source text so the layout is not
# broken by missing keys. In production, replace the fallback with human-
# reviewed text once it is available, or hold the render step until review
# completes.
render_payload = {}
for element_id, original_text in extracted_text.items():
    if element_id in accepted:
        render_payload[element_id] = accepted[element_id]
    else:
        render_payload[element_id] = original_text

# Step 6: Render (async)
# translatedFile is the modified ExtractedText.json with translated values, same keys
translated_json_bytes = json.dumps(render_payload).encode("utf-8")

render_resp = requests.post(
    f"{FOXIT_BASE}/documents/translation/render-pdf",
    headers=HEADERS,
    data={
        "sourceDocumentId": source_document_id,
        "preprocessResultDocumentId": preprocess_result_id,
        "targetLanguage": "fr"
        # Optional: "pageRangeStart": 1, "pageRangeEnd": 10
    },
    files={"translatedFile": ("ExtractedText.json", translated_json_bytes, "application/json")}
)

# Errors from render-pdf per the Foxit spec:
#   400 VALIDATION_ERROR    - "Either translatedFile or translatedTextDocumentId must be provided"
#   400 VALIDATION_ERROR    - "Unsupported target language: xx"
#   500 RENDER_START_FAILED - "Failed to start render: service unavailable"
render_resp.raise_for_status()
render_task_id = render_resp.json()["taskId"]

render_result = poll_task(render_task_id)
output_doc_id = render_result["resultDocumentId"]

# Step 7: Download translated PDF
pdf_resp = requests.get(
    f"{FOXIT_BASE}/documents/{output_doc_id}/download",
    headers=HEADERS
)
pdf_resp.raise_for_status()
with open("translated_output.pdf", "wb") as f:
    f.write(pdf_resp.content)

print(f"Done. Accepted: {len(accepted)}, Flagged: {len(flagged_for_review)}, Rejected: {len(rejected)}")

The render call is multipart/form-data. You pass sourceDocumentId (the original PDF’s document ID from step 1), preprocessResultDocumentId (from step 3), targetLanguage (one of the 23 supported codes), and translatedFile (the modified ExtractedText.json with translated values and original keys). The alternative is uploading the translated JSON first via the upload endpoint and passing its ID as translatedTextDocumentId instead. At least one of the two must be present, or you’ll get a 400 VALIDATION_ERROR.

The render operation is asynchronous. It returns 202 Accepted immediately with a taskId, and the actual rendering runs in the background on Foxit’s side. You must poll GET /tasks/{taskId} on a fixed interval, every 3 seconds is the recommended cadence, until the status flips to COMPLETED before you try to download the output. Skipping the poll, or treating the initial 202 response as if it were a finished render, will cause the program to crash and interrupt the rest of the pipeline because the result document is not yet written when the task is still IN_PROGRESS. The poll_task helper from the first snippet already implements this loop with a 3-second time.sleep between checks and surfaces a FAILED status as a RuntimeError, so reuse it here rather than reading render_resp.json() directly. The same polling discipline applies to the structural extract step (step 2), which is also asynchronous.

Scoring and Routing

Straker AI generates both the translation and the quality signal in this pipeline. Foxit’s responses carry document IDs and task statuses; the translation choice and the per-segment score are entirely Straker’s contribution.

Each segment in the /project/{id}/segments/{file_id}/{language_id} response carries three values you care about. target_text is Straker’s translation. score is a float between 0.0 and 1.0 (it may be null for segments where the model has no confidence signal). quality is a categorical label Straker assigns alongside the numeric score (bestgoodacceptable, or bad). You can route on either signal, or combine them. The table below shows a combined policy calibrated for compliance-sensitive documents. These are starting points; your production system should calibrate per language pair and domain, since a French legal contract demands different thresholds than a Spanish marketing brochure.

Straker verdictActionRationale
quality == "best" or score >= 0.85Auto-accept, include in renderHigh confidence output; suitable for fully automated workflows
quality in ("good", "acceptable") or 0.65 - 0.84Flag segment by element ID for post-edit reviewMedium confidence; a human reviewer checks the flagged segments before the final render runs
quality == "bad" or score < 0.65Reject segment, escalate to human translatorLow confidence output; the model is unreliable for this segment

The element ID key structure matters here. Foxit’s ExtractedText.json keys are packed into XLF trans-unit IDs, and Straker surfaces the same value in its response’s external_id field. That means every entry in your flagged_for_review dictionary carries enough information for a reviewer to open the source document, find the exact element by ID, and return an approved translation. You write the approved translation back into the same key, then trigger the render step. This produces a documentable audit trail. For every element ID in the output PDF, you can show the original text, Straker’s translation, the Straker score and quality label, and whether a human approved it. In regulated industries (finance, legal, healthcare), that’s the evidence your compliance team needs to sign off on an automated localization workflow, and it aligns with ISO 18587, the international standard for post-editing of machine translation output.

Straker AI can also route low-confidence output to expert reviewers automatically when configured through the Straker platform. Check straker.ai/ai-platform/verify for the workflow configuration options.

Layout Preservation

Foxit’s render step preserves multi-column text flow, embedded table cell structure, images at their original positions, headers and footers, and font substitution for target-language character sets. That means CJK scripts (Japanese, Chinese, Korean) render correctly with appropriate glyph substitution, and Arabic output renders right-to-left without manual post-processing.

StructureInfo.json is what makes this possible. When the preprocess step runs, it produces both the text map (which you hand to Straker) and the layout blueprint (which you hand back to Foxit unmodified at render time). The render engine maps translated text back to the original element positions using this blueprint, reflowing text within the same bounding boxes. Because the structure data travels alongside the text through the entire pipeline, Foxit never needs to reconstruct the layout from scratch.

Generic MT pipelines export raw text, losing all spatial relationships, translate it, then attempt to rebuild the PDF from nothing. Tables merge into continuous text, columns collapse to a single flow, and CJK font substitution fails because the rebuilding step has no record of what fonts were originally in use.

Limitations to Test

Text expansion is the first limitation worth stress-testing. English to German translation typically increases text length by 20-35%, and English to Arabic can run even longer. Foxit’s render engine handles reflow within bounding boxes, but extreme length changes in tight table cells or narrow columns may overflow. Test with your actual document types before you commit to a production deployment.

Complex layout edge cases are the second limitation. Overlapping text boxes, embedded SVG charts with text labels, and PDFs with non-standard encoding may produce imperfect renders. The structural extraction step covers standard PDF text elements well, but edge-case layouts require manual review of the rendered output before you sign off on the pipeline for a given document class.

Try It Now

Sign up for Foxit’s free Developer plan and a Straker AI account, grab credentials for both, and run the pipeline from the section above against a real document. An invoice, a multi-page contract, or a regulatory filing works well for testing because each has tables, mixed-column layouts, and high-stakes text segments.

After the render completes, verify four things in the output PDF:

  • Tables retain cell structure
  • Multi-column text flows correctly in the target language
  • Images remain in their original positions
  • Fonts render correctly for the target script

Cross-reference the confidence scores from Straker against the rendered segments to calibrate your production thresholds. You may find that legal terminology in German warrants a 0.90 auto-accept threshold while product description text in French is fine at 0.80.

The complete Foxit Translation Trial API reference covers the full parameter list and response schema for preprocess-pdf and render-pdf. The Foxit Structural Extraction Trial API reference documents the structural extract endpoint. Straker’s translation and scoring API documentation lives at straker.ai/ai-platform/verify.

Looking ahead, Straker’s dashboard lists a native Foxit integration as Coming Soon (no release date announced at the time of writing), described as a workflow to translate PDF contracts with Foxit, verify them with experts, and finalize them for signing. When it ships, it’s likely to compress several of the manual steps above into a single call. The underlying mechanics (structural extract, translation, per-segment scoring, routing, render) will remain the same logical stages, so the pipeline you build today stays a useful mental model for reasoning about the native version when it arrives.

For production-scale implementation patterns and how Straker’s translation and verification layer integrates into enterprise localization pipelines, register for the upcoming joint Foxit + Straker.ai webinar with Lee Konstanty from Straker. Get your Foxit API credentials | Get started with Straker AI

PDF Translation API FAQ

A PDF translation API with confidence scoring is a service that translates PDF documents and returns a per-segment quality signal alongside each translation. Instead of handing back a single translated file, the API tells you which segments are high-confidence (safe to auto-accept), which are medium-confidence (queue for human review), and which are low-confidence (escalate to a translator). This pipeline combines Foxit’s PDF Translation Trial API for structural extraction and layout-preserving rendering with Straker.ai for translation and scoring in a single call.

The pipeline runs in seven steps: upload the source PDF to Foxit, run structural extraction to get element-ID-keyed text, preprocess to produce ExtractedText.json and StructureInfo.json, send segments to Straker AI’s “AI Translation and Quality Evaluation” workflow which returns translated text plus a 0.0–1.0 score and a quality label, route each segment programmatically by score, then call Foxit’s render endpoint to rebuild the PDF in the original layout. Foxit owns PDF structure, Straker owns translation and scoring, and your code owns the routing decision.

For marketing copy, raw machine translation output is usually fine. For contracts, medical forms, clinical trial protocols, or regulatory filings, a 95%-accurate translation can still be legally dangerous because the 5% failure may land on a high-stakes clause — like “indemnification” rendered as “Entschädigung” (compensation) instead of “Freistellung” (hold harmless). Per-segment confidence scores let you route low-confidence segments to human reviewers before they reach the final document, producing the audit trail compliance teams need under standards like ISO 18587.

Yes. The translation step is pluggable. The contract upstream — Foxit element IDs mapped to source strings — and downstream — a dict of element IDs to translated text feeding the render call — does not change if you swap the engine. DeepL, Google Cloud Translation, AWS Translate, or an in-house NMT engine all work. The trade-off is that Straker AI returns translation plus quality score in one call, while other engines require a separate verification step if you want confidence signals.

Foxit’s preprocess step produces two files: ExtractedText.json with element-ID-keyed text, and StructureInfo.json with the full layout blueprint (bounding boxes, font metadata, column positions, image locations). You modify only ExtractedText.json with translations and pass StructureInfo.json to the render endpoint untouched. The render engine reflows translated text within the original bounding boxes, handles font substitution for CJK and Arabic scripts, and preserves multi-column layouts, tables, and image positions — without rebuilding the PDF from scratch.

Foxit’s render endpoint accepts 23 target language codes: en, zh, zh_tw, fr, de, es, it, pt, nl, ja, ko, th, vi, hi, ru, ar, tr, pl, sv, no, nb, da, and fi. Straker AI identifies languages by UUID rather than ISO code, fetched via GET /languages. Your production language coverage is the intersection of both sets — check both APIs before finalizing your language matrix.

A reasonable starting policy for compliance-sensitive documents: auto-accept segments with quality == “best” or score >= 0.85, flag for post-edit review at 0.65–0.84 or quality in (“good”, “acceptable”), and reject for human translation at score < 0.65 or quality == “bad”. These are starting points — calibrate per language pair and domain. A French legal contract may warrant a 0.90 auto-accept threshold while a Spanish marketing brochure is fine at 0.80. Run the pipeline against a representative sample of your real documents and tune from there.

Extract Anything from Any PDF: Inside Foxit’s Advanced Extraction Engine

Foxit PDF Structural Extraction API engine extracting tables, forms, and text from scanned PDFs.

Basic PDF extraction libraries break on scanned documents, complex tables, and form fields, leaving downstream pipelines starved of clean data. Foxit’s PDF Structural Extraction API combines OCR, layout recognition, and AI parsing to return all twelve PDF element types as structured JSON, ready for RAG, BI, and CRM workflows.

Your PDF extraction pipeline passes unit tests against the sample invoices you built it on. Then production arrives and you’re looking at 47% garbled output on the Q4 contract batch because half those documents are scanned TIFFs wrapped in a PDF envelope, and your extraction library has no concept of what an image-only page actually is.

The failure modes are specific. PyMuPDF’s get_text() returns empty strings on scanned PDFs because it reads content streams directly, and image-only pages carry no text stream. pdfplumber’s table detection merges rows when column widths span non-uniform grids, which is standard in any financial statement that mixes summary and line-item rows on the same page. Embedded images containing meaningful text (stamped signatures, engineering drawing annotations, letterhead logos) get silently dropped. The library extracts coordinates for the XObject reference but does nothing with the raster data inside. Form fields built on non-standard annotation types (AcroForms using widget annotations with custom action streams) lose their values entirely when you serialize to text.

The architectural distinction that creates this problem is the difference between content serialization and semantic extraction. A PDF converter reads a content stream and writes out whatever character sequences it finds in rendering order. An extraction engine understands the spatial relationships between those character sequences: that two columns of text at x=72 and x=320 are parallel body copy, that the row at y=210 belongs to the table starting at y=180, that the text block repeating on every page is a header carrying lower retrieval weight in a RAG index. Output that lacks spatial and semantic classification looks correct on screen but breaks every downstream consumer that depends on structure.

BI dashboards require numbers tied to the right row labels. AI ingestion pipelines require heading hierarchy to chunk accurately. CRMs require form field values extracted from AcroForm widget dictionaries, delivered with field names intact. The delta between what basic extraction libraries return and what those systems can actually consume is where document pipeline engineering hours accumulate.

How Foxit’s PDF Structural Extraction Engine Works Under the Hood

Foxit exposes this capability as the PDF Structural Extraction (Trial) endpoint inside the PDF Services API (POST /pdf-services/api/documents/pdf-structural-extract). Trial status means the schema is versioned at v1.0.7 and may evolve, but the contract is stable enough to build against today, and the endpoint runs against the production base URL at developer-api.foxit.com.

The engine runs three coordinated layers. The OCR layer operates on rasterized page content, recognizing characters from image-based PDFs and scanned documents across 200+ languages. The layout recognition layer applies spatial analysis to identify column boundaries, reading order, table cell boundaries, figure regions, and header/footer zones. The AI-based parsing layer classifies extracted objects semantically, resolving ambiguous blocks (a text run that spans two layout columns, or a figure caption that reads syntactically like a section heading) into typed elements.

All three layers run inside Foxit’s core PDF engine, which powers 700 million+ users across 20+ years of production deployments. That engine has native awareness of PDF internal structures: content streams, XObject dictionaries, AcroForm field trees, and annotation layers. The OCR layer operates on the same internal page representation the rendering engine uses, so it handles annotated PDFs where text overlaps image regions, and form fields where the visual display and stored value diverge.

The same Structural Extraction endpoint is also Step 1 of Foxit’s PDF Translation (Trial) workflow, which signals that the extraction output is structured enough to backbone a full rewrite-and-rerender pipeline.

NVIDIA’s July 2025 NeMo Retriever research on PDF extraction showed that specialized OCR-based pipelines outperform general-purpose vision-language models on retrieval recall and throughput for complex elements including tables, charts, and infographics. VLMs produce plausible-looking output on clean documents but degrade on exactly the edge cases (multi-column scans, mixed-content pages, annotated overlays) that a specialized pipeline handles systematically.

The Full Object Map: All 12 Extractable PDF Element Types

The Structural Extraction schema v1.0.7 defines twelve element types in the type enum: titleheadparagraphtableimageheaderFooterformhyperlinkfootnotesidebarannotation, and formula.

The API exposes no per-object filter parameters. The only request body fields are documentId (required) and password (optional, for protected PDFs). The engine extracts the full element graph and returns everything in one asynchronous round-trip. You filter client-side on the returned JSON. The design is correct for the workload because partial extraction would require re-running layout recognition per request, costing more compute than transmitting the full element set in a single ZIP.

The result is a ZIP archive. At minimum it contains StructureInfo.json, whose top-level analyzeResult object holds versionpageselements, and info. Documents that contain figures or tables also produce additional binary files (image renditions and table renditions) alongside the JSON, referenced from individual elements so the JSON payload stays manageable on large documents.

Each element in the document-wide flat elements array carries its own idtypecontentregion (with page and an 8-point boundingBox polygon), and score confidence value. A table element adds its cell grid. A form element adds field data. An image element points to its binary file in the ZIP. Because titlehead, and paragraph elements appear in document reading order in the elements array, they chunk cleanly on semantically correct boundaries, which is what a RAG index needs to return complete, coherent passages.

Each type maps directly to a downstream use case: table feeds financial reporting pipelines, form drives automated CRM data entry, image routes to computer vision workflows or document archives, annotation builds compliance audit trails, and head combined with paragraph elements in reading order feeds RAG ingestion.

API Walkthrough: The Four-Step Async PDF Extraction Flow

There’s no synchronous path. You upload, get a task ID, poll until completion, then download the result ZIP. Every request carries two headers: client_id and client_secret (lowercase snake_case, as specified in the API spec’s security schemes). Both come from the Developer Portal’s default application. Pass them as named HTTP headers on every request and do not use Authorization: Bearer.

The four-step sequence runs as follows:

Four-step PDF structural extraction API flow between client and Foxit PDF Services. 

The four-step sequence diagram uses two headers on every request: client_id and client_secret. Create a free developer account at account.foxit.com/site/sign-up (no credit card required, no sales call). Once you’re in, the credentials live under the default application in the Developer Portal. Copy the Client ID and Client Secret pair and treat them like any other API secret. Pass them as named HTTP headers on every call (lowercase snake_case, not Authorization: Bearer).

  • Step 1: Upload the PDF to POST /pdf-services/api/documents/upload as multipart/form-data with the file under field name file. The 100MB ceiling is enforced with a 413 and error code MAX_UPLOAD_SIZE_EXCEEDED. The response body returns { "documentId": "doc_abc123" }.

  • Step 2: Starts extraction with POST /pdf-services/api/documents/pdf-structural-extract, passing { "documentId": "doc_abc123" }. Add a "password" field for protected PDFs. The response is 202 Accepted with { "taskId": "task_xyz789" }.

  • Step 3: Polls GET /pdf-services/api/tasks/{task-id}. The TaskResponse carries taskIdstatusprogress (0-100 integer), resultDocumentId, and an optional error object. The status enum values are PENDINGIN_PROGRESSCOMPLETED, and FAILED. Portal narrative copy occasionally uses “PROCESSING,” but the schema enum value is IN_PROGRESS. Match your code against the enum. Poll until COMPLETED and capture resultDocumentId.

  • Step 4: Downloads with GET /pdf-services/api/documents/{resultDocumentId}/download, which streams the ZIP archive. The optional filename query parameter overrides the default filename.

The complete cURL sequence for all four steps: 

# Step 1: Upload
curl -X POST "https://na1.fusion.foxit.com/pdf-services/api/documents/upload" \
  -H "client_id: YOUR_CLIENT_ID" \
  -H "client_secret: YOUR_CLIENT_SECRET" \
  -F "file=@invoice_batch.pdf"

# {"documentId":"doc_abc123"}

# Step 2: Start extraction
curl -X POST "https://na1.fusion.foxit.com/pdf-services/api/documents/pdf-structural-extract" \
  -H "client_id: YOUR_CLIENT_ID" \
  -H "client_secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"documentId":"doc_abc123"}'

# 202 Accepted: {"taskId":"task_xyz789"}

# Step 3: Poll task status
curl "https://na1.fusion.foxit.com/pdf-services/api/tasks/task_xyz789" \
  -H "client_id: YOUR_CLIENT_ID" \
  -H "client_secret: YOUR_CLIENT_SECRET"

# {"taskId":"task_xyz789","status":"COMPLETED","progress":100,"resultDocumentId":"result_def456"}

# Step 4: Download the result ZIP
curl "https://na1.fusion.foxit.com/pdf-services/api/documents/result_def456/download" \
  -H "client_id: YOUR_CLIENT_ID" \
  -H "client_secret: YOUR_CLIENT_SECRET" \
  -o extraction_result.zip

The Python version with a polling loop and ZIP parsing:

import requests, json, time, zipfile
BASE_URL = "https://na1.fusion.foxit.com/pdf-services/api"
HEADERS  = {"client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET"}

# Step 1: Upload
with open("invoice_batch.pdf", "rb") as f:
    doc_id = requests.post(
        f"{BASE_URL}/documents/upload", headers=HEADERS, files={"file": f}
    ).json()["documentId"]

# Step 2: Start extraction
task_id = requests.post(
    f"{BASE_URL}/documents/pdf-structural-extract",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={"documentId": doc_id},
).json()["taskId"]

# Step 3: Poll until COMPLETED or FAILED
while True:
    task = requests.get(f"{BASE_URL}/tasks/{task_id}", headers=HEADERS).json()
    if task["status"] == "COMPLETED":
        result_doc_id = task["resultDocumentId"]
        break
    if task["status"] == "FAILED":
        raise RuntimeError(f"Extraction failed: {task.get('error')}")
    time.sleep(2)

# Step 4: Download the result ZIP and save it locally for inspection,
# then parse StructureInfo.json from the saved file
response = requests.get(
    f"{BASE_URL}/documents/{result_doc_id}/download", headers=HEADERS
)
with open("advanced-extraction-result.zip", "wb") as f:
    f.write(response.content)

with zipfile.ZipFile("advanced-extraction-result.zip") as zf:
    json_name = next(n for n in zf.namelist() if n.endswith("StructureInfo.json"))
    result = json.loads(zf.read(json_name))["analyzeResult"]

print(f"Schema: {result['version']['schema']}, Elements: {len(result['elements'])}")

On a clean run you should see output like Schema: 1.0.7, Elements: 9 for a small invoice batch. You’ll also find a fresh advanced-extraction-result.zip next to your script. That ZIP holds the full API response, including StructureInfo.json and any rendered image or table binaries, so you can inspect everything the engine returned and not just the parsed JSON.

First, set up and activate a Python virtual environment in your project folder. The official venv guide covers the exact commands for macOS, Linux, and Windows.

Once the virtualenv is active, the sample only needs one third-party package. Drop this into a requirements.txt next to your script and install it with pip install -r requirements.txt:

requests>=2.31.0

If you’re on macOS, use Homebrew Python (brew install python) rather than the system Python from the Xcode command-line tools. The Xcode build is linked against LibreSSL, which is enough to make a correct sample fail.
The ZIP contains a StructureInfo.json file whose top-level object wraps everything under analyzeResult. Inside that wrapper you get a version object, a pages array, a flat elements array, and an info block with analysis metadata. Each element carries its own idtypecontentregion (with page and an 8-point boundingBox polygon [x1,y1,x2,y2,x3,y3,x4,y4]), and a score confidence value:

{
  "analyzeResult": {
    "version": {
      "schema": "1.0.7",
      "software": "FoxitPDFAnalyzer",
      "model": "idp-analysis"
    },
    "pages": [
      {
        "pageNumber": 1,
        "size": { "width": 612, "height": 792, "unit": "point" },
        "state": "success"
      }
    ],
    "elements": [
      {
        "id": "title1",
        "type": "title",
        "content": {
          "text": "Q3 Revenue Summary",
          "style": {
            "fontName": "Helvetica",
            "fontSize": 24.0,
            "fontWeight": 0,
            "fontItalic": false
          }
        },
        "region": {
          "page": 1,
          "boundingBox": [72, 47, 317, 47, 317, 80, 72, 80]
        },
        "score": 0.76
      }
    ],
    "info": {
      "basicInfo": {
        "softwareVersion": "1.6.0",
        "analyzedPageCount": 1,
        "elementCounts": { "title": 1 }
      },
      "extendedMetadata": {
        "pageCount": 1,
        "isEncrypted": false,
        "hasAcroform": false,
        "language": "en"
      }
    }
  }
}

Elements of type tableimage, and form carry additional type-specific payload on top of this base shape, and any rendered image or table binary lands as a sibling file inside the ZIP referenced from the element.

HTTP errors return a standard error envelope:

{ "code": "VALIDATION_ERROR", "message": "documentId is required" }

The documented error codes include VALIDATION_ERROR (400), MAX_UPLOAD_SIZE_EXCEEDED (413), DOCUMENT_NOT_FOUND (404), STORAGE_ERROR, and INTERNAL_SERVER_ERROR (500).

Password-protected PDFs that arrive with no password parameter reach the processing stage before failing. That failure surfaces in the task status poll response after status reaches FAILED, so your error handler must inspect the task response body in addition to the HTTP status codes from the initial POST calls:

{
  "taskId": "task_xyz789",
  "status": "FAILED",
  "progress": 0,
  "error": {
    "code": "INTERNAL_SERVER_ERROR",
    "message": "Document is password-protected"
  }
}

Wiring Extracted PDF Data Into Your Workflow

Pattern 1: AI/RAG pipeline. Filter the flat elements array to titlehead, and paragraph types. Chunk by heading hierarchy, iterating over the array in the order the engine returned it (document reading order is preserved across columns and pages). Embed each chunk and index in Pineconepgvector, or your vector store of choice. Correct reading order, as provided by the extraction engine, is the prerequisite for accurate RAG retrieval on multi-column and paginated documents. When chunks split mid-thought because a layout detector merged two columns, retrieval recall drops and answer quality follows.

Pattern 2: BI reporting. Filter elements by type == "table" client-side, then convert each table’s cell structure into a pandas DataFrame:

import pandas as pd

# `result` is the `analyzeResult` object loaded from StructureInfo.json
tables = [e for e in result["elements"] if e["type"] == "table"]

for i, tbl in enumerate(tables):
    # Cells live at content.body.cells[]. Each cell carries rowIndex,
    # columnIndex, and a nested paragraph whose content.text holds the value.
    body = tbl["content"]["body"]
    grid = [["" for _ in range(body["columnCount"])] for _ in range(body["rowCount"])]
    for cell in body.get("cells", []):
        text = cell.get("paragraph", {}).get("content", {}).get("text", "")
        grid[cell["rowIndex"]][cell["columnIndex"]] = text
    df = pd.DataFrame(grid[1:], columns=grid[0])  # first row as header
    print(f"Table {i}: {df.shape[0]} rows x {df.shape[1]} cols")
    # df.to_gbq("finance.q3_revenue", project_id="your-project")  # BigQuery
    # df.to_sql("q3_revenue", engine)                             # Postgres / Snowflake

The row and column indices from the extraction schema map directly to DataFrame positions, so you get a correctly-structured table with zero manual parsing.

Pattern 3: n8n automation. The four-step flow maps to a chain of HTTP Request nodes in n8n. The first node uploads to POST .../upload and passes documentId through the item. The second sends POST .../pdf-structural-extract and captures taskId. A Loop Over Items construct with an HTTP Request node calling GET .../tasks/{taskId} on a two-second interval checks status until COMPLETED, then routes to the download node. The final HTTP Request node calls GET .../documents/{resultDocumentId}/download, and a Code node using n8n’s binary data helpers unpacks the ZIP and parses the JSON for routing to a Salesforce, HubSpot, Postgres, or Airtable node. The polling requirement makes this a multi-node workflow, but you write zero custom glue code and gain n8n’s built-in error routing and retry handling.

PDF Extraction Tools Compared: Foxit vs. Adobe, Google, Amazon, and Azure

ToolUnderlying ApproachEcosystem Lock-inHandles Scanned PDFsPricing ModelSetup OverheadStatus
Foxit Structural ExtractionProprietary OCR + layout recognition + AI (integrated core engine)Cloud-agnostic REST APIYes (dedicated OCR layer)Subscription, no per-page creditsLow (2 credential headers, 4 REST calls)Trial (schema v1.0.7)
Adobe PDF Extract APIAdobe Sensei ML, reading order + renditionsAdobe Document ServicesYesContact salesMedium (Adobe SDK + ecosystem)GA
Google Document AICloud ML + generative AI, Document Object ModelGoogle Cloud requiredYesPer-page pay-as-you-goMedium-high (GCP + IAM)GA
Amazon TextractDeep learning OCR, key-value and table extractionAWS-nativePartial (strong on forms, weaker on complex layouts)Per-page pay-as-you-goMedium (AWS + IAM)GA
Azure Document IntelligencePrebuilt + custom ML modelsAzure ecosystemYes (prebuilt models)Per-page + model training costsHigh for custom modelsGA

Google Document AI and Azure Document Intelligence win on ecosystem integration if you’re all-in on those clouds. Adobe wins on PDF structural fidelity for workflows already inside the Adobe Document Services ecosystem. Amazon Textract excels on standardized form documents where its pre-trained schema fits the input. These are real advantages, and the comparison is honest only when those contexts are acknowledged.

Foxit’s case is strongest when you need a cloud-agnostic REST API with zero ecosystem dependency, full object coverage across all twelve element types, and enterprise throughput (10 to 10,000+ PDFs/day) with SOC 2, GDPR, and HIPAA compliance built in. The Structural Extraction status is a real trade-off to factor in. The schema at v1.0.7 is callable and stable enough for pipeline integration today, but GA competitors carry a finalized contract. Pin your parser to the version field in the response and you’re insulated from schema evolution.

Your First PDF Extraction API Call, Right Now

Go to developer-api.foxit.com, create a free developer account (no credit card required), and copy your Client ID and Client Secret from the default application. Use the built-in API Playground or import the Postman collection from the Developer Portal to run the four-step sequence: upload a real document (an invoice, a multi-page contract, or a scanned form), call pdf-structural-extract with the returned documentId, poll tasks/{taskId} until COMPLETED, then download via documents/{resultDocumentId}/download.

Unzip the result, open StructureInfo.json, and check three things: analyzeResult.version.schema should report 1.0.7analyzeResult.elements[] should contain at least one table element and one form element if your source document includes those, and the ZIP root should contain the corresponding binary files for any image-type elements. That verification confirms the full extraction pipeline is wired correctly end-to-end.

The same endpoint pattern scales to enterprise volumes. Increase upload and poll concurrency horizontally and the architecture stays identical, with no schema changes, no infrastructure modifications, and no per-page credit consumption to track.

The engineering gap between what basic extraction libraries return and what downstream systems actually consume is where document pipeline hours accumulate. Structural Extraction closes that gap at the API layer, so the complexity stays in the engine and out of your codebase. Get started at developer-api.foxit.com.

PDF Structural Extraction FAQ

PDF structural extraction is the process of identifying and classifying the semantic elements inside a PDF, such as titles, paragraphs, tables, forms, images, and annotations, rather than just pulling raw text. Foxit’s PDF Structural Extraction API returns twelve distinct element types as structured JSON, preserving spatial relationships, reading order, and table cell grids so downstream systems like RAG pipelines, BI dashboards, and CRMs can consume the data without manual parsing.

Yes. Foxit’s PDF Structural Extraction engine includes a dedicated OCR layer that recognizes characters from image-based and scanned PDFs across 200+ languages. The OCR runs on the same internal page representation as the rendering engine, so it handles edge cases like text overlapping image regions, stamped signatures, and engineering drawing annotations that basic libraries like PyMuPDF silently drop.

Foxit’s API is cloud-agnostic with no ecosystem lock-in, requiring just two credential headers and four REST calls. Adobe PDF Extract requires the Adobe Document Services ecosystem, Google Document AI requires GCP and IAM setup, and Amazon Textract requires AWS infrastructure. Foxit also uses subscription-based pricing without per-page credits, while Google, AWS, and Azure all charge per page.

The API identifies twelve element types: title, head, paragraph, table, image, headerFooter, form, hyperlink, footnote, sidebar, annotation, and formula. Each element returns with its content, an 8-point bounding box polygon, page location, and a confidence score. Tables include full cell grids with row and column indices, forms include field data, and images are extracted as separate binary files inside the result ZIP.

The API uses a four-step asynchronous flow: upload the PDF via POST /documents/upload to get a documentId, start extraction with POST /documents/pdf-structural-extract, poll GET /tasks/{taskId} every two seconds until status is COMPLETED, then download the result ZIP via GET /documents/{resultDocumentId}/download. Authentication uses two headers, client_id and client_secret, available from the default application in the Foxit Developer Portal.

The endpoint is currently in Trial status with schema version v1.0.7, meaning the contract is stable but may evolve. It runs on the production base URL at developer-api.foxit.com and is built on Foxit’s core PDF engine, which powers 700 million+ users across 20+ years of deployments. For production pipelines, pin your parser to the version field in the response to insulate against future schema changes.