HTML to PDF API: Building Production-Grade Conversion Pipelines with Foxit PDF Services

Learn how to build a production-grade HTML to PDF pipeline using Foxit PDF Services API.

Automate HTML to PDF conversion with Foxit’s API. Build scalable pipelines to replace Puppeteer, handle bulk processing, and ensure reliable document generation.

Your Puppeteer setup works fine at low volume. You launch a Chrome process, load the page, call page.pdf(), and write the bytes to disk. Clean enough. Then your invoice generation hits 500 documents per night, your report export feature goes live in three time zones simultaneously, and the wheels start coming off. Chrome processes time out waiting for JavaScript hydration. Memory climbs until your container OOMs. The font that renders correctly on your MacBook looks wrong on the Linux build server. You spend a Friday afternoon tuning networkidle2 timeouts per template instead of shipping features.

This is the failure mode of treating a rendering engine as a conversion service. Headless Chrome is a browser. Running it at production document volume means you’re operating a browser fleet: process pooling, memory isolation, crash recovery, rendering consistency across OS environments. That infrastructure overhead comes directly out of engineering time.

The architectural alternative is a managed REST API: POST your HTML (or a URL), let the service render the PDF, and download the result. The rendering infrastructure becomes the API provider’s problem. This guide covers how to build that conversion pipeline end-to-end using Foxit PDF Services API, from authentication through batch processing and production error handling.

The Production Problem with Headless Browser PDF Conversion

A standard Puppeteer setup looks like this:

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle2" });
const pdf = await page.pdf({ format: "A4", printBackground: true });
await browser.close();

At five documents a day, this is fine. At five hundred concurrent, each puppeteer.launch() spins up a full Chromium process (roughly 100-200MB RSS on Linux). If you’re running in a container with 2GB of memory and you get 20 concurrent requests, you’re at the limit before accounting for the Node.js process itself or any other application memory.

The standard solution is a Chrome process pool (libraries like puppeteer-cluster or generic-pool). Now you’re managing pool size tuning, handling pool exhaustion under burst traffic, and writing cleanup logic for crashed Chrome instances. You’ve added significant operational complexity to what started as a one-liner.

Font rendering is a separate category of pain. Chrome on macOS uses CoreText. Chrome on Linux uses FreeType with fontconfig. The same CSS font-family: 'Inter' declaration produces visibly different output depending on whether Inter is installed as a system font or loaded via a @font-face declaration, and whether the fallback stack resolves differently across environments. Teams that ship invoice PDFs to customers discover this in production, not in development.

JavaScript execution adds another dimension. If your page renders a data table via a React component that fetches data on mount, networkidle2 is not a reliable wait condition. Network activity can go idle before the DOM has finished updating. You end up tuning waitForSelector or adding arbitrary timeouts per template, and those timeouts become technical debt that breaks when the page changes.

The architectural fix isn’t a better Puppeteer wrapper. It’s offloading the entire rendering layer to a service that was built to handle it reliably: a managed REST API with consistent rendering environments, predictable behavior, and no infrastructure for your team to maintain.

How Cloud HTML-to-PDF APIs Handle Rendering

Cloud conversion APIs typically accept input in two modes: URL mode and file upload mode.

In URL mode, you pass a public URL. The API fetches the page, renders it, and returns a PDF. This works when your page is publicly accessible and all assets (fonts, images, stylesheets) load from the same domain or CDN. The tradeoff is that the API’s rendering environment must reach your server, which creates a dependency on network reachability and your server’s response time. If you’re generating PDFs from an internal dashboard behind a VPN, URL mode doesn’t work without additional networking.

In file upload mode, you construct the complete HTML file (with inlined CSS and assets where needed) and upload it to the API. The service processes the file and returns a PDF. This eliminates the external asset dependency and makes your conversion more deterministic: the same HTML file always produces the same PDF, regardless of what’s deployed on your web server at the time.

Beyond input mode, rendering fidelity depends on several factors:

  • CSS @media print rules control what renders into the PDF. Navigation bars, sidebars, and hover states should be hidden via print stylesheets so they don’t appear in the output.
  • Font loading strategy determines rendering consistency. Relying on system fonts produces different output across environments. Embedding fonts via @font-face with a CDN URL or base64-inlined data guarantees consistent rendering.
  • Page layout properties (paper size, margins, orientation) can be controlled through CSS @page rules embedded in the HTML itself. This keeps layout configuration in the document rather than in API parameters.
  • JavaScript execution matters for pages that render content dynamically. Some APIs wait for the page to stabilize before capturing; others capture immediately.

These factors are the same ones you’d manage with Puppeteer’s page.pdf() options, but with a cloud API you handle them through your HTML/CSS rather than through in-process code.

Setting Up Foxit PDF Services API: Authentication and First Conversion

Foxit PDF Services API is a cloud-hosted REST API built on Foxit’s proprietary PDF engine, backed by over 20 years of PDF technology development. Create an account at the Foxit Developer Portal (the Developer plan is free, includes 500 credits/year, and requires no credit card). Generate your API credentials (a client_id and client_secret) from the Developer Dashboard.

Understanding the Async Workflow

Unlike a simple request-response API, Foxit PDF Services uses an asynchronous task-based workflow. Every operation follows the same pattern:

  1. Submit the job (upload a file, or POST a URL)
  2. Receive a taskId in the response
  3. Poll the task status until it completes or fails
  4. Download the result using the resultDocumentId from the completed task

This design handles long-running operations gracefully. A complex HTML page might take several seconds to render; the async pattern means your client never blocks on a single HTTP request waiting for rendering to finish.

URL-to-PDF Conversion

For pages that are publicly accessible, URL-to-PDF is the simplest path. You POST the URL directly and the API fetches, renders, and converts it. Here’s the complete workflow in Python using the requests library:

import os
import requests
from time import sleep

HOST = os.environ["FOXIT_API_HOST"]  # e.g., https://na1.fusion.foxit.com
CLIENT_ID = os.environ["FOXIT_CLIENT_ID"]
CLIENT_SECRET = os.environ["FOXIT_CLIENT_SECRET"]

AUTH_HEADERS = {
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
}


def create_url_to_pdf_task(url: str) -> str:
    """Submit a URL for PDF conversion. Returns a taskId."""
    headers = {**AUTH_HEADERS, "Content-Type": "application/json"}
    response = requests.post(
        f"{HOST}/pdf-services/api/documents/create/pdf-from-url",
        json={"url": url},
        headers=headers,
    )
    response.raise_for_status()
    return response.json()["taskId"]


def poll_task(task_id: str, interval: int = 5) -> dict:
    """Poll until the task completes or fails. Returns the task status object."""
    headers = {**AUTH_HEADERS, "Content-Type": "application/json"}
    while True:
        response = requests.get(
            f"{HOST}/pdf-services/api/tasks/{task_id}",
            headers=headers,
        )
        response.raise_for_status()
        status = response.json()
        if status["status"] == "COMPLETED":
            return status
        elif status["status"] == "FAILED":
            raise RuntimeError(f"Task {task_id} failed: {status}")
        sleep(interval)


def download_document(document_id: str, output_path: str) -> None:
    """Download the resulting PDF by its document ID."""
    response = requests.get(
        f"{HOST}/pdf-services/api/documents/{document_id}/download",
        headers=AUTH_HEADERS,
        stream=True,
    )
    response.raise_for_status()
    with open(output_path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)


# Full workflow: URL to PDF
task_id = create_url_to_pdf_task("https://example.com/invoice/1042")
result = poll_task(task_id)
download_document(result["resultDocumentId"], "invoice_1042.pdf")
print("PDF generated successfully.")

In this code, you define three reusable functions that map to the async workflow: create_url_to_pdf_task() submits a public URL and returns a taskIdpoll_task() checks the task status in a loop until it reaches COMPLETED or FAILED, and download_document() streams the resulting PDF to disk. The final three lines wire them together into the complete conversion pipeline.

Before running: Set your FOXIT_API_HOSTFOXIT_CLIENT_ID, and FOXIT_CLIENT_SECRET environment variables with the values from your Foxit Developer Dashboard. Never commit credentials to source control; use environment variables or a secrets manager.

HTML File-to-PDF Conversion

When your content isn’t publicly accessible (internal dashboards, dynamically generated reports), you can upload an HTML file directly. This follows the standard 4-step async pattern:

def upload_document(file_path: str) -> str:
    """Upload a file to Foxit. Returns a documentId."""
    with open(file_path, "rb") as f:
        response = requests.post(
            f"{HOST}/pdf-services/api/documents/upload",
            files={"file": f},
            headers=AUTH_HEADERS,
        )
    response.raise_for_status()
    return response.json()["documentId"]


def create_html_to_pdf_task(document_id: str) -> str:
    """Create an HTML-to-PDF conversion task. Returns a taskId."""
    headers = {**AUTH_HEADERS, "Content-Type": "application/json"}
    response = requests.post(
        f"{HOST}/pdf-services/api/documents/create/pdf-from-html",
        json={"documentId": document_id},
        headers=headers,
    )
    response.raise_for_status()
    return response.json()["taskId"]


# Full workflow: HTML file to PDF
doc_id = upload_document("report.html")
task_id = create_html_to_pdf_task(doc_id)
result = poll_task(task_id)
download_document(result["resultDocumentId"], "report.pdf")
print("HTML converted to PDF successfully.")

In this code, you first upload a local .html file via upload_document(), which returns a documentId referencing the uploaded file on Foxit’s servers. Then create_html_to_pdf_task() submits that documentId for conversion. The rest of the workflow is identical: poll for completion, then download the result.

Note: Replace "report.html" with the path to your own HTML file. This code reuses the poll_task() and download_document() functions from the URL-to-PDF example above, so make sure both are defined in the same script.

The key difference: URL-to-PDF skips the upload step (you POST the URL directly), while HTML file conversion requires uploading the .html file first via the /documents/upload endpoint. Both use the same poll-and-download pattern after task creation.

Refer to the Foxit API documentation and the Postman workspace for the complete parameter reference, including any additional rendering options supported by these endpoints. The GitHub demo repository contains working examples in Python, Node.js, and PHP.

Controlling CSS and JavaScript Rendering in HTML-to-PDF Conversion

Regardless of which API you use for HTML-to-PDF conversion, the quality of the output depends on how well you prepare the HTML. The rendering parameters live in your document, not in API request fields.

The single most common rendering problem between “looks right in a browser” and “looks wrong in a PDF” is the CSS media type. By default, browsers render with screen styles, which means your navigation bar, sidebar, and hover states all appear. For PDF output, you want your @media print rules to take over.

Write your print styles explicitly:

@media print {
  nav,
  .sidebar,
  .no-print {
    display: none;
  }

  body {
    font-size: 11pt;
    font-family: "Inter", Arial, sans-serif;
    color: #000;
  }

  .invoice-table {
    page-break-inside: avoid;
  }

  .page-header {
    page-break-before: always;
  }

  @page {
    size: A4;
    margin: 20mm 15mm;
  }
}

In this stylesheet, you hide non-essential UI elements (navigation, sidebars) when printing, set a clean body font, and use page-break-inside: avoid to prevent the renderer from splitting a table row across pages. The nested @page rule sets the paper size and margins at the CSS level, so layout configuration stays in the document rather than in API parameters.

For font rendering consistency, don’t rely on system fonts. Include a @font-face declaration in your HTML that loads from a CDN, or inline the font as base64:

<style>
  @font-face {
    font-family: "Inter";
    src: url("https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ.woff2")
      format("woff2");
    font-weight: 400;
    font-style: normal;
  }
</style>

In this snippet, you embed the Inter font directly in the HTML using a @font-face declaration that points to Google Fonts. This guarantees Inter renders in the PDF regardless of what fonts are installed in the API’s container environment. The tradeoff is latency: the rendering engine fetches the font file during conversion. If you’re running high-volume batch jobs, consider inlining the font as a base64 data URI to eliminate that network round trip.

For JavaScript-heavy pages, make sure the content has fully rendered before the API captures it. If you’re using the URL-to-PDF endpoint, the API fetches and renders the live page, so your page’s JavaScript will execute. For the HTML file upload path, keep your HTML self-contained with all data already rendered in the markup rather than relying on client-side JavaScript to populate it after load.

Batch HTML-to-PDF Conversion at Scale

Sequential conversion is the naive starting point:

for invoice in invoices:
    doc_id = upload_document(invoice.html_path)
    task_id = create_html_to_pdf_task(doc_id)
    result = poll_task(task_id)
    download_document(result["resultDocumentId"], f"output/{invoice.id}.pdf")

In this loop, each invoice is processed one at a time: upload, convert, poll, download, then move to the next. Each iteration blocks on the poll loop before starting the next conversion. At a few seconds per document (upload, render, poll, download), 500 invoices could take over 30 minutes.

The fix is concurrent dispatch with a semaphore to cap parallelism. Check your plan’s rate limits before setting the semaphore ceiling in production.

import asyncio
import aiohttp
import os
from pathlib import Path

HOST = os.environ["FOXIT_API_HOST"]
CLIENT_ID = os.environ["FOXIT_CLIENT_ID"]
CLIENT_SECRET = os.environ["FOXIT_CLIENT_SECRET"]
MAX_CONCURRENT = 10  # Adjust based on your plan's rate limits


async def convert_one(
    session: aiohttp.ClientSession,
    sem: asyncio.Semaphore,
    invoice_id: str,
    html_path: str,
    output_dir: Path,
) -> tuple[str, bool]:
    async with sem:
        try:
            auth = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}

            # Step 1: Upload the HTML file
            with open(html_path, "rb") as f:
                form = aiohttp.FormData()
                form.add_field("file", f, filename="document.html")
                async with session.post(
                    f"{HOST}/pdf-services/api/documents/upload",
                    data=form,
                    headers=auth,
                ) as resp:
                    if resp.status != 200:
                        return invoice_id, False
                    upload_result = await resp.json()
                    doc_id = upload_result["documentId"]

            # Step 2: Create the conversion task
            async with session.post(
                f"{HOST}/pdf-services/api/documents/create/pdf-from-html",
                json={"documentId": doc_id},
                headers={**auth, "Content-Type": "application/json"},
            ) as resp:
                if resp.status != 200:
                    return invoice_id, False
                task_result = await resp.json()
                task_id = task_result["taskId"]

            # Step 3: Poll for completion
            while True:
                async with session.get(
                    f"{HOST}/pdf-services/api/tasks/{task_id}",
                    headers={**auth, "Content-Type": "application/json"},
                ) as resp:
                    status = await resp.json()
                    if status["status"] == "COMPLETED":
                        result_doc_id = status["resultDocumentId"]
                        break
                    elif status["status"] == "FAILED":
                        print(f"Task failed for {invoice_id}")
                        return invoice_id, False
                await asyncio.sleep(5)

            # Step 4: Download the result
            async with session.get(
                f"{HOST}/pdf-services/api/documents/{result_doc_id}/download",
                headers=auth,
            ) as resp:
                if resp.status == 200:
                    pdf_bytes = await resp.read()
                    (output_dir / f"{invoice_id}.pdf").write_bytes(pdf_bytes)
                    return invoice_id, True
                return invoice_id, False

        except Exception as e:
            print(f"Error converting {invoice_id}: {e}")
            return invoice_id, False


async def batch_convert(invoices: list[dict], output_dir: str = "output") -> dict:
    output_path = Path(output_dir)
    output_path.mkdir(exist_ok=True)

    sem = asyncio.Semaphore(MAX_CONCURRENT)
    connector = aiohttp.TCPConnector(limit=MAX_CONCURRENT)

    async with aiohttp.ClientSession(connector=connector) as session:
        tasks = [
            convert_one(session, sem, inv["id"], inv["html_path"], output_path)
            for inv in invoices
        ]
        results = await asyncio.gather(*tasks)

    succeeded = [r[0] for r in results if r[1]]
    failed = [r[0] for r in results if not r[1]]
    return {"succeeded": len(succeeded), "failed": failed}


# Usage
invoices = [
    {"id": "inv_1042", "html_path": "templates/invoice_1042.html"},
    {"id": "inv_1043", "html_path": "templates/invoice_1043.html"},
    # ... up to thousands of entries
]

result = asyncio.run(batch_convert(invoices))
print(f"Converted {result['succeeded']} PDFs. Failed: {result['failed']}")

In this code, you use asyncio and aiohttp to process multiple HTML-to-PDF conversions concurrently. The convert_one() function runs the full 4-step workflow (upload, create task, poll, download) for a single invoice, while batch_convert() dispatches all invoices in parallel, capped by a semaphore. Results are collected via asyncio.gather() and split into succeeded and failed lists.

Before running: Set FOXIT_API_HOSTFOXIT_CLIENT_ID, and FOXIT_CLIENT_SECRET as environment variables with your credentials from the Developer Dashboard. Adjust MAX_CONCURRENT based on your plan’s rate limits, and update the invoices list with your actual file paths.

With MAX_CONCURRENT = 10 and several seconds per conversion (including polling), the batch processes 10 documents at a time instead of one at a time. The semaphore prevents you from flooding the API with simultaneous requests and hitting the rate limit ceiling. Beyond aiohttp, no additional dependencies are needed since asyncio is part of Python’s standard library.

Credit consumption at scale: the Developer plan includes 500 credits/year. The Startup plan ($1,750/year) provides 3,500 credits. Each conversion typically costs 1 credit. For higher volumes, the Business plan ($4,500/year) includes 150,000 credits. Check your remaining credit balance via the Developer Dashboard before launching a large batch job.

For volumes beyond what a single process can handle efficiently, a queue-based architecture decouples submission from processing. Services like Amazon SQS or Redis Streams handle the message brokering:

App Server → Message Queue (SQS / Redis Streams) → Worker Pool (N workers)
  Worker: upload HTML → create task → poll → download PDF → store in S3/GCS
  Worker: update job status in Postgres / Redis

Each worker picks a job from the queue, runs the 4-step conversion workflow, writes the resulting PDF to S3 or GCS, and updates the job status in a database. This pattern handles burst volume naturally: jobs queue up during spikes, workers drain at the rate the API allows, and your app server is never blocked waiting for conversions to complete.

Production Deployment Patterns for HTML-to-PDF Pipelines

Error Handling and Retry Logic

Not all errors warrant a retry. Map HTTP status codes to decisions before writing any retry logic.

400 Bad Request means your request body is malformed. Retrying the same payload returns another 400. Fix the payload, don’t retry. A 429 Too Many Requests and a 503 Service Unavailable are transient: back off and retry. A FAILED task status means the conversion itself failed (possibly due to invalid HTML or unreachable URLs); check the task response for diagnostic details.

import time
import random
import requests
from requests.exceptions import RequestException

PERMANENT_ERRORS = {400, 401, 403, 422}
TRANSIENT_ERRORS = {429, 500, 502, 503, 504}


def post_with_retry(
    url: str,
    max_retries: int = 4,
    base_delay: float = 1.0,
    **kwargs,
) -> requests.Response:
    """POST with exponential backoff and jitter for transient errors."""
    for attempt in range(max_retries + 1):
        try:
            response = requests.post(url, timeout=60, **kwargs)

            if response.status_code in range(200, 300):
                return response

            if response.status_code in PERMANENT_ERRORS:
                raise ValueError(
                    f"Permanent error {response.status_code}: {response.text}"
                )

            if response.status_code in TRANSIENT_ERRORS:
                if attempt == max_retries:
                    raise RuntimeError(
                        f"Max retries exceeded. Last status: {response.status_code}"
                    )
                delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
                print(f"Transient error {response.status_code}. Retrying in {delay:.1f}s...")
                time.sleep(delay)

        except RequestException as e:
            if attempt == max_retries:
                raise
            delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
            time.sleep(delay)

    raise RuntimeError("Unexpected: exhausted retries without returning or raising")


# Usage with the URL-to-PDF endpoint
auth_headers = {
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
    "Content-Type": "application/json",
}

response = post_with_retry(
    f"{HOST}/pdf-services/api/documents/create/pdf-from-url",
    json={"url": "https://example.com/invoice/1042"},
    headers=auth_headers,
)
task_id = response.json()["taskId"]

In this code, you wrap every POST request in a retry loop with exponential backoff. The function distinguishes between permanent errors (like 400 or 401, which should not be retried) and transient errors (like 429 or 503, which resolve on their own). Each retry doubles the wait time and adds random jitter to avoid synchronized retry waves.

Before running: Replace CLIENT_IDCLIENT_SECRET, and HOST with your Foxit credentials and API host, or load them from environment variables as shown in the earlier examples.

The jitter (random.uniform(0, 0.5)) prevents a thundering herd where every worker wakes up and retries simultaneously after a 429 burst. Without it, plain exponential backoff still produces synchronized retry waves when all workers hit the rate limit at the same time.

Output Optimization: Compression and Linearization

After conversion, you can chain additional PDF operations using the same async pattern. Upload the resulting PDF, call the compression or linearization endpoint, poll, and download the optimized version.

For PDFs served directly in a browser, linearization enables Fast Web View, which lets the browser display page one while the rest of the file downloads:

def compress_and_linearize(input_pdf_path: str, output_path: str) -> None:
    """Compress a PDF, then linearize it for fast web viewing."""
    auth = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}
    json_headers = {**auth, "Content-Type": "application/json"}

    # Upload the PDF
    doc_id = upload_document(input_pdf_path)

    # Compress
    resp = requests.post(
        f"{HOST}/pdf-services/api/documents/modify/pdf-compress",
        json={"documentId": doc_id, "compressionLevel": "MEDIUM"},
        headers=json_headers,
    )
    resp.raise_for_status()
    task = poll_task(resp.json()["taskId"])
    compressed_doc_id = task["resultDocumentId"]

    # Linearize the compressed result (no need to re-upload; use the resultDocumentId)
    resp = requests.post(
        f"{HOST}/pdf-services/api/documents/optimize/pdf-linearize",
        json={"documentId": compressed_doc_id},
        headers=json_headers,
    )
    resp.raise_for_status()
    task = poll_task(resp.json()["taskId"])

    # Download the final optimized PDF
    download_document(task["resultDocumentId"], output_path)

In this code, you chain two PDF operations back-to-back. First, you upload the PDF and compress it at MEDIUM level (valid options are LOWMEDIUM, and HIGH). Once compression completes, you pass the resultDocumentId directly into the linearization step, which avoids a second upload. The final download gives you a PDF that is both smaller and optimized for progressive loading in browsers.

Note: This function reuses upload_document()poll_task(), and download_document() from the earlier examples. Make sure those functions are defined in the same script with your credentials configured. The Foxit developer blog post on chaining PDF actions covers this pattern in detail.

Monitoring and Secret Management

Track three metrics per conversion job: latency (to detect API degradation), credit consumption per job type (to project when you’ll exhaust your plan), and failure rate by error code (to catch template regressions before they hit customers). Set an alert when remaining credits drop below 20% of your plan allocation. The Foxit Developer Dashboard exposes real-time usage data you can check before launching batch runs.

API credentials go in environment variables or a secrets manager (AWS Secrets ManagerHashiCorp VaultGCP Secret Manager). Rotate credentials from the Developer Dashboard when team members leave or when you suspect a credential has been exposed. You can generate new credentials and revoke old ones without a service interruption if you update your environment first.

Run Your First HTML-to-PDF Conversion

Sign up for the Foxit Developer plan at no cost, no credit card, with 500 credits available immediately. Generate your client_id and client_secret from the Developer Dashboard. Clone the demo repository for working examples in Python, Node.js, and PHP, or copy the URL-to-PDF example from this guide and run it against a public page.

After your first conversion completes, check your credit usage in the Dashboard to validate your throughput estimate and cost projection for production volume. The Startup plan ($1,750/year for 3,500 credits) is self-serve with no sales call required if you need more capacity.

Start building for free on the Foxit Developer Portal

Document Generation API: Automate Personalized Document Creation at Scale

Document Generation API: Automate Personalized Document Creation at Scale

Automate document creation at scale with a document generation API. Learn how templates and JSON data replace manual workflows to produce PDFs instantly.

Somewhere in your company right now, someone is copying client data from a CRM into a Word document, updating the logo, adjusting the date, and exporting it to PDF. If you’re lucky, it’s an intern doing this for 50 documents a month. If you’re not, it’s a developer who hard-coded the layout in iText or PDFKit, and Marketing is about to ask them to change the font size.

Neither of those is a document generation strategy. They’re workarounds — and like legacy Mail Merge tools that choke on anything beyond a few hundred records, they don’t scale to 50,000 invoices overnight.

The good news is that the problem is well-solved. A document generation API lets you treat documents exactly like you treat any other data pipeline: a template defines the structure, a JSON payload carries the content, and the API outputs a polished, production-ready PDF (or DOCX) in milliseconds. The copy-paste step disappears entirely.

This guide walks through how that works, where it fits in real-world systems, and how tools like Foxit’s DocGen API make the implementation straightforward — even for a team that’s never touched a document automation system before.

What Is a Document Generation API?

document generation API is a cloud service that merges a template with structured data to produce a final document — typically a PDF or DOCX. The template defines layout, fonts, branding, and placeholder tokens. A JSON payload supplies the dynamic values. The API engine renders the finished document in milliseconds, with no manual intervention and no layout code required.

The equation looks like this:

Template (Structure) + JSON Data (Content) + API Engine = Final Document

Using a document generation API rather than building a local generator offers two key advantages: scalability and separation of concerns.

On the scalability side, the same POST request that generates one invoice generates 100,000 invoices. You don’t need to provision more rendering capacity, manage memory pressure from large PDFs, or debug pagination edge cases — the API handles it. On the separation-of-concerns side, your legal team can update a liability clause in the Word template without touching your codebase. Your marketing team can swap the logo without a redeploy. The document’s design is fully decoupled from the application logic that drives it.

That said, not all document generation tools take the same approach. Older solutions like PDFKit or Apache PDFBox require you to code the visual layout programmatically — drawing lines, positioning text boxes, calculating column widths manually. That works for simple, static documents. It breaks down fast when tables grow dynamically, when conditional sections appear based on customer data, or when stakeholders want to iterate on the design.The API approach flips that model, the design stays in the template while the logic stays in the API.

The Architecture of Document Generation Automation

Understanding the three-layer architecture makes every implementation decision easier. Here’s how the pieces fit together.

1. Template Creation

With a modern document generation API like Foxit’s, you don’t write rendering code. You open Microsoft Word, design the document exactly as it should look, and insert double-bracket tokens wherever dynamic data belongs.

A simple invoice template might include:

  • {{ companyName }} — replaced with the client’s company name
  • {{ invoiceDate \@ MM/dd/yyyy }} — replaced with a date formatted as 01/15/2024
  • {{ totalDue \# Currency }} — replaced with a currency-formatted number like $2,500.00

That’s it. Business users can open this .docx file, update the header font, move the logo, reword a clause — and none of those changes require a developer.

2. Data Binding

Your application pulls data from wherever it lives — a Salesforce CRM, an SAP ERP, a PostgreSQL database — and structures it as a JSON payload. The JSON keys map directly to the template token names. No transformation layer, no intermediate format.

A payload for the invoice above looks like:

{
  "companyName": "Meridian Financial Group",
  "invoiceDate": "2024-01-15",
  "invoiceNumber": "INV-00471",
  "lineItems": [
    {
      "description": "API Integration Consulting",
      "qty": 10,
      "unitPrice": 150.0
    },
    { "description": "Compliance Review", "qty": 5, "unitPrice": 200.0 }
  ],
  "totalDue": 2500.0
}

The keys companyNameinvoiceDate, and totalDue match the tokens in the template. It’s a direct 1:1 bind.

3. Dynamic Template Logic: Loops and Formatting

This is where document generation APIs separate themselves from simple find-and-replace tools.

Repeating tables are handled with loop delimiters. In Foxit’s syntax, you wrap the repeating row of your Word table with {{TableStart:lineItems}} and {{TableEnd:lineItems}}. The API iterates over the lineItems array in your JSON and generates one row per item — no matter if the array has 2 entries or 200. Inside the loop, you can use {{ ROW_NUMBER }} for automatic line numbering and {{ SUM(ABOVE) }} to calculate column totals.

Formatting specifiers are built into the token syntax. Currency formatting (\# Currency) converts 2500.00 to $2,500.00. Date formatting (\@ MM/dd/yyyy) handles locale-specific date presentation without extra preprocessing in your application code.

The result: dynamic document templates that handle variable-length tables, formatted numbers, and conditional logic entirely in Word — not in your codebase.

Top Document Generation Use Cases by Industry

Document generation sits in the critical path for several industries. These are the scenarios where teams see the most immediate return.

Financial Services: Client Reports and Investment Summaries

A wealth management firm generates quarterly performance summaries for thousands of clients. The template — header, chart placeholders, disclaimer text, signature block — stays constant. The data changes per client: portfolio value, allocation breakdown, benchmark comparison, YTD return. The team pushes a nightly batch job that pulls data from their portfolio management system, constructs JSON payloads per client, and fires POST requests to the generation API — programmatic invoice generation and report creation at scale. By morning, 8,000 personalized PDFs are sitting in an Amazon S3 bucket, ready to be emailed.

Insurance: The Policy Packet

An insurance carrier issuing a homeowner’s policy needs to assemble a multi-section document: the cover letter (personalized with the policyholder’s name and address), the policy declarations page (premium, coverage limits, deductibles), the endorsements, and the liability disclaimer. Each section might be its own template. The API merges them into a single PDF. Underwriters stop manually assembling packets; the system generates them at bind time.

HR and Operations: Employee Onboarding

When a new hire accepts an offer, the HRIS triggers a webhook. The document generation service receives the employee’s data — name, role, start date, salary, benefits elections — and generates the full onboarding packet: offer letter, benefits summary, I-9 instructions, handbook acknowledgment. The employee receives a complete, personalized PDF bundle within seconds of accepting. No one in HR touched it.

Sales: Branded Quotes and Contracts

Sales teams often live in the “Export to PDF” nightmare — they fill in a spreadsheet, copy it into Word, format it manually, and hope the branding looks right. A document generation API replaces that with a contract automation workflow tied directly to the CRM. When a rep marks a deal as “Proposal Sent,” Salesforce triggers a POST request with the deal data, and the API returns a PDF with the correct pricing, the client’s logo pulled from the CRM, and the correct contract terms based on the deal tier.

Foxit DocGen: The Developer Experience

Foxit brings 20+ years of PDF engine experience to a word template to PDF API that’s straightforward to integrate. The implementation follows three steps:

Step 1 : Design your Word template

Add {{ token }} placeholders using Foxit’s double-bracket syntax. Format specifiers and loop delimiters go directly into the Word document. No special editor required.

Step 2: Send a POST request

The endpoint is /document-generation/api/GenerateDocumentBase64. You authenticate with your client_id and client_secret in the request headers, then send the base64-encoded template and your JSON data in the body. Here’s what that looks like in Python:

import requests
import base64

# Load and encode the Word template
with open("invoice_template.docx", "rb") as f:
    template_b64 = base64.b64encode(f.read()).decode("utf-8")

# JSON data payload from your CRM or database
document_values = {
    "companyName": "Meridian Financial Group",
    "invoiceDate": "2024-01-15",
    "invoiceNumber": "INV-00471",
    "lineItems": [
        {"description": "API Integration Consulting", "qty": 10, "unitPrice": 150.00},
        {"description": "Compliance Review", "qty": 5, "unitPrice": 200.00}
    ],
    "totalDue": 2500.00
}

# POST to Foxit DocGen API
# Replace HOST with the base URL from your Foxit developer console
response = requests.post(
    "{HOST}/document-generation/api/GenerateDocumentBase64",
    headers={
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET",
        "Content-Type": "application/json"
    },
    json={
        "base64FileString": template_b64,
        "documentValues": document_values,
        "outputFormat": "pdf"
    }
)

# Decode and save
result = response.json()
pdf_bytes = base64.b64decode(result["base64FileString"])
with open("invoice_00471.pdf", "wb") as f:
    f.write(pdf_bytes)

print("Invoice generated successfully.")

Step 3: Decode and store the response

The API returns a base64-encoded document. Decode it, write it to disk or pass it to an Amazon S3 bucket, and you’re done.

One feature worth calling out: Foxit supports DOCX output in addition to PDF. Most document generation APIs only produce PDFs — which means once the document is generated, it’s immutable. DOCX output enables a “Draft Mode” workflow where a generated document can be sent to a reviewer for light edits before it’s finalized. That’s genuinely useful in legal and HR workflows where a human needs to add a handwritten note or approve a clause. Sample code and SDKs are available for Python, JavaScript, Java, C#, and PHP, with additional language support on the developer portal. A Postman workspace is available for quick testing without writing any code first.

Why the API Approach Wins Over Build-It-Yourself

If you automate PDF creation with the API approach, you gain three concrete advantages over build-it-yourself alternatives:

  • Compliance and accuracy: When a developer hard-codes a PDF layout, human error enters every time someone updates the template. A token mismatch ({{ CustomerName }} vs. {{ customerName }}) silently renders as blank. The API approach catches mismatches and enforces consistent data binding. More importantly, the data flows directly from your database to the document — no manual copy-paste step, no transposition errors on a loan amount or policy limit.

  • Speed at scale: Generating PDFs with a local library like PDFKit means each document renders sequentially on your server — and rendering time grows with document complexity, especially when tables span multiple pages. At 10,000 documents, even modest per-document rendering times add up to minutes of blocking processing. A cloud document generation API parallelizes rendering across infrastructure you don’t manage. Fifty thousand invoices overnight is a scheduling problem, not a compute problem.

  • Maintenance belongs to the right person: When Marketing wants to update the invoice footer, they open the Word template and make the change. When Legal updates the liability clause, same thing. The developer does nothing — no code change, no redeploy, no regression testing on a layout that changed by three pixels. That’s the real ROI of the template-first approach, and it compounds over time as the team iterates on document designs without creating developer tickets.

Final Thoughts

Document generation shouldn’t be a manual task or a maintenance burden. If your team is still copy-pasting data into Word or maintaining a custom PDF renderer that breaks whenever a table gets too long, the template-plus-API model is a straightforward fix — not a major architectural change.

The next step worth watching: document generation paired with generative AI. The pattern is emerging in a few places — use an LLM to draft a personalized summary paragraph based on a client’s portfolio data, then pass the result as a JSON field to the document generation API. The LLM handles the prose; the API handles the formatting and branding. You get dynamic content without losing layout control.

Ready to automate your document workflow? Create a free developer account at Foxit and access the DocGen API today. Use the quickstart guide and Postman workspace to generate a PDF from JSON in minutes — and see for yourself why the template-plus-API approach is replacing hand-coded generators.

Building Auditable, AI-Driven Document Workflows with Foxit APIs

Building Auditable, AI-Driven Document Workflows with Foxit APIs

We had an incredible time at API World 2025 connecting with developers, sharing ideas, and seeing how Foxit APIs power everything from AI-driven resume builders to interactive doodle apps. In this post, we’ll walk through the same hands-on workflow Jorge Euceda demoed live on stage—showing how to build an auditable, AI-powered document automation system using Foxit PDF Services and Document Generation APIs.

This year’s API World was packed with energy—and it was amazing meeting so many developers face-to-face at the Foxit booth. We spent three days trading ideas about document automation, AI workflows, and integration challenges.

Our team hosted a hands-on workshop and sponsored the API World Hackathon, where developers submitted 16 high-quality projects built with Foxit APIs. Submissions ranged from:

  • Automated legal-advice generators

  • Compatibility-rating apps that analyze your personality match

  • AI-powered resume optimizers that tailor your CV to dream-job descriptions

  • Collaborative doodle games that turn drawings into shareable PDFs

Each project offered a new perspective on what’s possible with Foxit APIs—and we loved seeing the creativity.

Among all the sessions, Jorge Euceda’s workshop stood out as a crowd favorite. It showed how to make AI document decisions auditable, explainable, and replayable using event sourcing and two key Foxit APIs. That’s exactly what we’ll walk through below.

Click here to grab the project overview file.

Prefer to follow along with the live session instead of reading step-by-step?
Watch Jorge’s complete “AI-Powered Resume to Report” presentation from API World 2025.
It includes every step shown below—plus real-time API responses.

What You’ll Build

A complete, auditable workflow:

Resume Upload → Extract Resume Data → AI Candidate Scoring → Generate HR Report → Event Store

This workshop is designed for technical professionals and managers who want to learn how to use application programming interfaces (APIs) and explore how AI can enhance document workflows. Attendees will get hands-on experience with Foxit’s PDF Services (extraction/OCR) and Document Generation APIs, and see how event sourcing turns AI decisions into an auditable, replayable ledger.

By the end, you’ll have a Python-based demo that extracts data from a PDF resume, analyzes it against a policy, and generates a polished HR Report PDF with a traceable event log.

Getting Set Up

To follow along, you’ll need:

  • Access to a terminal with a Python 3.9+ Environment and internet connectivity

  • Visual Studio Code or your preferred IDE

  • Basic familiarity with REST/JSON (helpful but not required)

 

  1. Install Dependencies
python -V
# virtual environment setup, requests installation
python3 -m venv myenv
source myenv/bin/activate
pip3 install requests
  1. Download the project’s zip file below

Project Source Code

Now extract the files somewhere in your computer, open in Visual Studio Code or your preferred IDE.

You may use any sample resume PDF for inputs/input_resume.pdf. A sample one is provided, but you may leverage any resume PDF you wish to generate a report on.

  1. Create a Foxit Account for credentials

Create a Free Developer Account now or navigate to our getting started guide, which will go over how to create a free trial.

Hands-On Walkthrough

Step 1 – Open the Project

Now that you’ve downloaded the workshop source code, navigate to the resume_to_report.py file, which will serve as our main entry point.

Once dependencies are installed and the ZIP file extracted, open your workspace and run:

python3 resume_to_report.py

You should see console logs showing:

  • An AI Report printed as JSON

  • A generated PDF (outputs/HR_Report.pdf)

  • An event ledger (outputs/events.json) with traceable actions

Step 2 — Inspect the outputs

Open the generated HR report to review:

  • Candidate name and phone

  • Overall fit score

  • Matching skills & gaps

  • Summary and policy reference in the footer

Then open events.json to see your audit trail—each entry captures the AI’s decision context.

{
  "eventType": "DecisionProposed",
  "traceId": "8d1e4df6-8ac9-4f31-9b3a-841d715c2b1c",
  "payload": {
    "fitScore": 82,
    "policyRef": "EvaluationPolicy#v1.0"
  }
}

This is your audit trail.

Step 3 — Replay & Explain a Policy Change

Replay demonstrates why event-sourcing matters:

  1. Edit inputs/evaluation_policy.json: add a hard requirement (e.g., "kubernetes") or adjust the job_description emphasis.

  2. Re-run the script with the same resume.

  3. Compare:

    • New decision and updated PDF content

    • Event log now reflects the updated rationale (PolicyLoaded snapshot → new DecisionProposed with the same traceId lineage)

  4. Emphasize: The input resume hasn’t changed; only policy did — the event ledger explains the difference.

Policy: Drive Auditable & Replayable Decisions

The AI assistant uses a JSON policy file to control how it scores, caps, and summarizes results. Every policy snapshot is logged as its own event, creating a replayable audit trail for governance and compliance.

 

{
  "policyId": "EvaluationPolicy#v1.0",
  "job_description": "Looking for a software engineer with expertise in C++, Python, and AWS cloud services. Experience building scalable applications in agile teams; familiarity with DevOps and CI/CD.",
  "overall_summary": "Make the summary as short as possible",
  "hard_requirements": ["C++", "python", "aws"]
}

Notes:

  • policyId appears in both the report and event log.

  • job_description defines what the AI is looking for.

  • Changing these values creates a new traceable event.

Generate a Polished Report

Next, use the Foxit Document Generation API to fill your Word template and create a formatted PDF report.

Open inputs/hr_report_template.docx, you will find the following HR reporting template with placeholders for the fields we will be entering:

Tips:

  • Include lightweight branding (logo/header) to make the generated PDF presentation-ready.

  • Include a footer with traceable Policy ID and Trace ID Events

Results and Audit Trail

Here’s what the final HR Report PDF looks like:

Every decision has a Trace ID and Policy Ref, so you can recreate the report at any time and verify how the AI arrived there.

Why Event-Sourced AI Matters

This pattern does more than score resumes—it proves that AI decisions can be transparent, deterministic, and trustworthy.
By using Foxit APIs to extract, analyze, and generate documents, developers can bring auditability to any workflow that relies on machine logic.

Key Takeaways

  • Auditability – Every AI step emits a verifiable event.

  • Replayability – Change a policy and regenerate for deterministic results.

  • Explainability – Decisions carry policy and trace references for clear “why.”

  • Automation – PDF Services and Document Generation handle the document lifecycle end-to-end.

Try It Yourself

Ready to build your own auditable AI workflow?

Closing Thought

At API World, we set out to show how Foxit APIs can power real, transparent AI workflows—and the community response was incredible. Whether you’re building for HR, legal, finance, or creative industries, the same pattern applies:

Make your AI explain itself.

Start with the Foxit APIs, experiment with policies, and turn every AI decision into a traceable event that builds trust.

Create Custom Invoices with Word Templates and Foxit Document Generation

Create Custom Invoices with Word Templates and Foxit Document Generation

Invoicing is a critical part of any business. This tutorial shows how to automate the process by creating dynamic, custom PDF invoices with the Foxit Document Generation API. Learn how to design a Microsoft Word template with special tokens, prepare your data in JSON, and then use a simple Python script to generate your final invoices.

Create Custom Invoices with Word Templates and Foxit Document Generation

Invoicing is a critical part of any business, often involving multiple steps—gathering customer data, calculating amounts owed, and sending out invoices so your company can get paid. Foxit’s Document Generation API streamlines this process by making it easy to create well-formatted, dynamic PDF invoices. Let’s walk through an example.

Before You Start

If you want to follow along with this blog post, be sure to get your free credentials over on our developer portal. Also, read our introductory blog post, which covers the basics of working with our API.

As a reminder, the API makes use of Microsoft Word templates. These templates are essentials tokens wrapped in double brackets. When you call the API, you’ll pass the template and your data. Our API then dynamically replaces those tokens with your data and returns you a nice PDF (you can also get a Word file back as well).

Creating Your Custom Invoice with Word Templates

Let’s begin by designing the template in Word. An invoice typically includes things like:

  • The customer receiving the invoice
  • The invoice number and issue date
  • The payment due date
  • A detailed list of items, including name, quantity, and price for each line item, with a total at the end

The Document Generation API makes no requirements in terms of how you design your templates. Size, alignment, and so forth, can match your corporate styles and be as fancy, or simple, as you like. Let’s consider the template below (I’ll link to where you can download this file at the end of the article):

MS Word template

Let's break it down from the top.

  • The first token, {{ invoiceNum }}, represents the invoice number for the customer.
  • The next token is special. {{ today \@ MM/dd/yyyy }} represents two different features of the Document Generation API. First, today is a special value representing the present time, or more accurately, when you call the API. The next portion represents a date mask for representing a date value. Our docs have a list of available masks.
  • {{ accountName }} is another regular token.
  • The payment date, {{ paymentDueDate \@ MM/dd/yyyy }}, shows how the date mask feature can be used on dates in your own data as well.
  • Now let's look at the table. You can format tables however you like, but a common setup includes one row for the header and one row for the dynamic data. (In this example, there’s also a third row, which I'll explain shortly.) To start, you’ll use a marker tag: {{TableStart:lineItems}}, where lineItems represents an array in your data. The row ends with the matching {{TableEnd:lineItems}} tag. Between these two tags, you'll place additional tags for each value in the array. For example, we have a product, qty, price, and totalPrice for each item. You'll also see the special ROW_NUMBER value, which automatically counts each row starting at 1. Finally, the \# Currency format is applied to the totalPrice value to display it as a currency.
  • The last row in the table uses two special features together, namely SUM(ABOVE), which maps to creating a total of the last column from the table. This can be paired with currency formatting as shown.

Alright, now that you've seen the template, let's talk data!

The Data for Your Custom Invoices

Usually the data for an operation like this would come from a database, or perhaps an API with an ecommerce system. For this demo, the data will come from a simple JSON file. Let's take a look at it:

[
{
	"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 data consists of an array of 3 sets of invoice data. Each set follows the same pattern and matches what you saw above in the Word template. The only exception being the accountNumber value which wasn't used in the template. That's fine – sometimes your data will include things not necessary for the final PDF. In this case, though, we're actually going to make use of it (you'll see in a moment). Onward to code!

Calling the Foxit API with Our Data

Now for my favorite part – actually calling the API. The Generate Document API is incredibly simple; needing just your credentials, a base64 version of the template, and your data. The entire demo is slightly over 50 lines of Python code, so let's look at the template and then break it down.

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 the necessary modules and loading credentials from the environment, we define a simple docGen method. This method takes the template, data, and credentials, then calls the API endpoint. The API responds with the rendered PDF in Base64 format, which the method returns.

The main code of the template breaks down to:

  • Reading in the template and converting it to base64.
  • Reading in the JSON file
  • Iterating over each block of invoice data and calling the API
  • Remember how I said accountNumber wasn't used in the template? We actually use it here to generate a unique filename. Technically, you don't need to store the results at all. You could take the raw binary data and email it. But having a copy of the results does mean you can re-use it later, such as if the customer is late to pay.

Here's an example of one of the results:

Example PDF result

Next Steps

If you want to try this demo yourself, first grab yourself a shiny free set of credentials and then head over to our GitHub to grab the template, Python, and sample output values yourself.

Convert Office Docs to PDFs Automatically with Foxit PDF Services API

Convert Office Docs to PDFs Automatically with Foxit PDF Services API

See how to build a powerful, automated workflow that converts Office documents (Word, Excel, PowerPoint) into PDFs. This step-by-step guide uses the Foxit PDF Services API, the Pipedream low-code platform, and Dropbox to create a seamless “hands-off” document processing system. We’ll walk through every step, from triggering on a new file to uploading the final PDF.

Convert Office Docs to PDFs Automatically with Foxit PDF Services API

With our REST APIs, it is now possible for any developer to set up an integration and document workflow using their language of choice. But what about workflow automations? Luckily, this is even simpler (of course, depending on platform) as you can rely on the workflow service to handle a lot the heavy lifting of whatever automation needs you may have. In this blog post, I’m going to demonstrate a workflow making use of Pipedream. Pipedream is a low-code platform that lets you build flexible workflows by piecing together various small atomic steps. It’s been a favorite of mine for some time now, and I absolutely recommend it. But note that what I’ll be showing here today could absolutely be done on other platforms, like n8n.

Want the televised version? Catch the video below:

Our Office Document to PDF Workflow

Our workflow is based on Dropbox folders and handles automatic conversion of Office docs to PDFs. To support that, it does the following:

  • Listen for new files in a Dropbox folder
  • Do a quick sanity check (is it in the input subdirectory and an Office file)
  • Download the file to Pipedream
  • Send it to Foxit via the Upload API
  • Kick off the appropriate conversion based on the Office type
  • Check status via the Status API
  • When done, download the result to Pipedream
  • And finally, push it up to Dropbox in an output subdirectory

Here’s a nice graphical representation of this workflow:

Workflow chart

Before we get into the code, note that workflow platforms like Pipedream are incredibly flexible. When I build workflows with platforms like this I try to make each step as atomic, and focused as possible. I could absolutely have built a shorter, more compact version of this workflow. However, having it broken out like this makes it easier to copy and modify going forward (which is exactly how this one came about, it was based on a simpler, earlier version).

Ok, let's break it down, step-by-step.

Getting Triggered

In Pipedream, workflows begin with a trigger. While there are many options for this, my workflow uses a "New File From Dropbox" trigger. I logged into Dropbox via Pipedream so it had access to my account. I then specified a top level folder, "Foxit", for the integration. Additionally, there are two more important settings:

  • Recursive – this tells the trigger to file for any new file under the root directory, "Foxit". My Dropbox Foxit folder has both an input and output directory.
  • Include Link – this tells Pipedream to ensure we get a link to the new file. This is required to download it later.
Trigger details

Filtering the Document Flow

The next two steps are focused on filtering and stopping the workflow, if necessary. The first, end_if_output, is a built-in Pipedream step that lets me provide a condition for the workflow to end. First, I'll check the path value from the trigger (the path of the new file) and if it contains "output", this means it's a new file in the output directory and the workflow should not run.

Declaring the end condition

The next filter is a code step that handles two tasks. First, it checks whether the new file is a supported Office type—.docx, .xlsx, or .pptx—using our APIs. If the extension isn’t one of these, the workflow ends programmatically.

Later in the workflow, I’ll also need that same extension to route the request to the correct endpoint. So the code handles both: validation and preservation of the extension.

import os 

def handler(pd: "pipedream"):
  base, extension = os.path.splitext(pd.steps['trigger']['event']['name'])

  if extension == ".docx":
    api = "/pdf-services/api/documents/create/pdf-from-word"
  elif extension == ".xlsx":
    api = "/pdf-services/api/documents/create/pdf-from-excel"
  elif extension == ".pptx":
    api = "/pdf-services/api/documents/create/pdf-from-ppt"
  else:
    return pd.flow.exit(f"Exiting workflow due to unknow extension: {extension}.")

  return { "api":api }

As you can see, if the extension isn't valid, I'm exiting the workflow using pd.flow.exit (while also logging out a proper message, which I can check later via the Pipedream UI). I also return the right endpoint if a supported extension was used. This will be useful later in the flow.

Download and Upload API Data

The next two steps are primarily about moving data from the input source (Dropbox) to our API (Foxit).

The first step, download_to_tmp, uses a simple Python script to transfer the Dropbox file into the /tmp directory for use in the workflow

import requests

def handler(pd: "pipedream"):
    download_url = pd.steps["trigger"]["event"]["link"]
    file_path = f"/tmp/{pd.steps['trigger']['event']['name']}"

    with requests.get(download_url, stream=True) as response:
      response.raise_for_status()
      with open(file_path, "wb") as file:
          for chunk in response.iter_content(chunk_size=8192):
            file.write(chunk)
            
    return file_path

Notice at the end that I return the path I used in Pipedream. This action then leads directly into the next step of uploading to Foxit via the Upload API:

import os 
import requests 

def handler(pd: "pipedream"):
  clientid = os.environ.get('FOXIT_CLIENT_ID')
  secret = os.environ.get('FOXIT_CLIENT_SECRET')
  HOST = os.environ.get('FOXIT_HOST')
  
  headers = {
    "client_id":clientid,
    "client_secret":secret
  }

  with open(pd.steps['download_to_tmp']['$return_value'], 'rb') as f:
    files = {'file': (pd.steps['download_to_tmp']['$return_value'], f)}

    request = requests.post(f"{HOST}/pdf-services/api/documents/upload", files=files, headers=headers)

    return request.json()

The result of this will be a documentId value that looks like so:

{
  "documentId": "<string>"
}

Pipedream lets you define environment variables and I've made use of them for my Foxit credentials and host. Grab your own free credentials here!

Converting the Document Using the Foxit API

The next step will actually kick off the conversion. My workflow supports three different input types (Word, PowerPoint, and Excel). These map to three API endpoints. But remember that earlier we sniffed the extension of our input and set the endpoint there. Since all three APIs work the same, that's literally all we need to do – hit the endpoint and pass the document value from the previous step.

import os 
import requests 

def handler(pd: "pipedream"):

  clientid = os.environ.get('FOXIT_CLIENT_ID')
  secret = os.environ.get('FOXIT_CLIENT_SECRET')
  HOST = os.environ.get('FOXIT_HOST')
  
  headers = {
    "client_id":clientid,
    "client_secret":secret,
    "Content-Type":"application/json"
  }

  body = {
    "documentId": pd.steps['upload_to_foxit']['$return_value']['documentId']
  }

  api = pd.steps['extension_check']['$return_value']['api']
  
  print(f"{HOST}{api}")
  request = requests.post(f"{HOST}{api}", json=body, headers=headers)
  return request.json()
The result of this call, and nearly all of the Foxit APIs, will be a task:
{
  "taskId": "<string>"
}

Checking Your Document API Status

The next step is one that may take a few seconds – checking the job status. Foxit's endpoint returns a value like so:

{
  "taskId": "<string>",
  "status": "<string>",
  "progress": "<int32>",
  "resultDocumentId": "<string>",
  "error": {
    "code": "<string>",
    "message": "<string>"
  }
}
To use this, I just hit the API, check for status, and if it’s not done, wait five seconds and call it again. Here’s the Python code for this:
import os 
import requests 
from time import sleep 

def handler(pd: "pipedream"):

  clientid = os.environ.get('FOXIT_CLIENT_ID')
  secret = os.environ.get('FOXIT_CLIENT_SECRET')
  HOST = os.environ.get('FOXIT_HOST')
  
  headers = {
    "client_id":clientid,
    "client_secret":secret,
    "Content-Type":"application/json"
  }

  done = False
  while done is False:

    request = requests.get(f"{HOST}/pdf-services/api/tasks/{pd.steps['create_conversion_job']['$return_value']['taskId']}", headers=headers)
    status = request.json()
    if status["status"] == "COMPLETED":
      done = True
      return status
    elif status["status"] == "FAILED":
      print("Failure. Here is the last status:")
      print(status)
      return pd.flow.exit("Failure in job")
    else:
      print(f"Current status, {status['status']}, percentage: {status['progress']}")
      sleep(5)

As shown, errors are simply logged by default—but you could enhance this by adding notifications, such as emailing an admin, sending a text message, or other alerts.

On success, the final output is passed along, including the key value we care about: resultDocumentId.

Download and Upload – Again

Ok, if the workflow has gotten this far, it's time to finish the process. The next step handles downloading the result from Foxit using the download endpoint:

import requests
import os

def handler(pd: "pipedream"):
  clientid = os.environ.get('FOXIT_CLIENT_ID')
  secret = os.environ.get('FOXIT_CLIENT_SECRET')
  HOST = os.environ.get('FOXIT_HOST')

  headers = {
    "client_id":clientid,
    "client_secret":secret,
  }

  # Given a file of input.docx, we need to use input.pdf
  base_name, _ = os.path.splitext(pd.steps['trigger']['event']['name'])
  path = f"/tmp/{base_name}.pdf"
  print(path) 
  
  with open(path, "wb") as output:
		
    bits = requests.get(f"{HOST}/pdf-services/api/documents/{pd.steps['check_job']['$return_value']['resultDocumentId']}/download", stream=True, headers=headers).content 
    output.write(bits)
            
    return {
      "filename":f"{base_name}.pdf",
      "path":path
    }

Note that I'm using the base name of the input, which is basically the filename minus the extension. So for example, input.docx will become input, which I then slap a pdf extension on to create the filename used to store locally to Pipedream.

Finally, I push the file back up to Dropbox, but for this, I can use a built-in Pipedream step that can upload to Dropbox. Here's how I configured it:

  • Path: Once again, Foxit
  • File Name: This one's a bit more complex, I want to store the value in the output subdirectory, and ensure the filename is dynamic. Pipedream lets you mix and match hard-coded values and expressions. I used this to enable that: output/{{steps.download_result_to_tmp.$return_value.filename}}. In this expression the portion inside the double bracket will be dynamic based on the PDF file generated previously.
  • File Path: This is an expression as well, pointing to where I saved the file previously: {{steps.download_result_to_tmp.$return_value.path}}
  • Mode: Finally, the mode attribute specifies what to do on a conflict. This setting will be based on whatever your particular workflow needs are, but for my workflow, I simply told Dropbox to overwrite the existing file.

Here's how that step looks configured in Pipedream:

Upload step

Conclusion

Believe it or not, that's the entire workflow. Once enabled, it runs in the back ground and I can simply place any files into my Dropbox folder and my Office docs will be automatically converted. What's next? Definitely get your own free credentials and check out the docs to get started. If you run into any trouble at all, hit is up on the forums and we'll be glad to help!

Embed Secure eSignatures into Your App with Foxit API

Embed Secure eSignatures into Your App with Foxit API

Foxit eSign makes electronic signatures easy, but developers can take it further by automating the process. This tutorial shows how to use the Foxit eSign API to embed secure eSignatures in your apps. With Python code examples, you’ll learn to send documents for signing, dispatch reminders, and check the signing status programmatically.

Embed Secure eSignatures into Your App with Foxit API

Foxit eSign is an electronic signature solution that enables individuals and businesses to securely sign, send, and manage documents online. It streamlines the document signing process by allowing users to create legally binding eSignatures, prepare forms, and track document status in real time. With features like reusable templates, automated workflows, and audit trails, it enhances productivity and reduces the need for manual paperwork.

At the simplest level, a user can log into the eSign dashboard and handle 100% of their signing needs. So for example, they can upload a Microsoft Word template and then drag and drop fields that will be used during the signing process. I did this with a simple Word document. After uploading, I was given an easy to use editor to drag and drop fields:

Picture of eSign editor

In the screen shot above, I’ve added three fields to my document. The first is a date field. The second is for the signers name. The last field is the actual signature spot. Each of these fields have many options, and your own documents could have far more (or heck, even less) fields. The point is – you’re allowed to design these forms to meet whatever need you may have. Also note that you can do all of this directly within Word, as well. Our docs show how to add fields directly into Word that will become enabled in the signing process.

Once you’ve got your template in a nice place, you can initiate the signing process right from the app. The dashboard also gives you a full history and audit trail of that process (have they signed yet? when did they do it? who signed?) which is handy. But, of course, you’re here because you’re a developer, and you’re probably asking yourself – can we automate this? The answer? Of course!

If you would rather watch an introduction to the API, enjoy the video below!

eSign Via API

Before we begin digging into the APIs, be sure to take a quick look at the API Reference. As I’ve stated a few times, the signing process itself can be incredibly complex. For example, there may be 2, 3, or more people who need to sign a document, and in a particular order. Also, remember that I shared that the template process of adding fields and such can be done entirely in Word itself. This is to say that our look here is going to focus on a simple example of signing. But there’s nothing to stop the creation of more advanced, flexible workflows.

The first step in any API usage will be authentication. When you have an eSign account with API access, you’ll be given a client_id and client_secret value, both of which need to be exchanged for an access token at the appropriate endpoint. Here’s a simple example of this with Python:

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

def getAccessToken(id, secret):
	url = "https://na1.foxitesign.foxit.com/api/oauth2/access_token"
	payload=f"client_id={id}&client_secret={secret}&grant_type=client_credentials&scope=read-write"
	headers = {
	'Content-Type': 'application/x-www-form-urlencoded'
	}

	response = requests.request("POST", url, headers=headers, data=payload)

	token = (response.json())["access_token"]
	return token

access_token = getAccessToken(CLIENT_ID, CLIENT_SECRET)

All the rest of the demos will make use of this method, and at the end of this post I'll share GitHub links for the source.

Kicking Off the Signing API Process

Now that we've authenticated with the API, our code can start doing… well, everything. The first thing we will add is a signing process with the template I showed above. From the dashboard itself I was able to make note of the template ID, 392230, but note that there's APIs for working with templates where that could be done via code as well.

To start the signing process, we can use the Create Envelope from Template endpoint. You can think of an envelope as a set of documents a user has to sign. For our demo, it's one, but you can include multiple documents. If you look at the API reference example, you'll see a large input body. As I've said, and will probably keep saying, the electronic signing process can be quite complex. For our simple demo, however, we just need to know the name of the person we're asking to sign the document and their email address. I whipped up this simple Python utility to enable that:

def sendForSigning(template_id, first_name, last_name, email, token):
	url = "https://na1.foxitesign.foxit.com/api/templates/createFolder"
	body = {
		"folderName":"Sending for Signing",
		"templateIds":[template_id],
		"parties":[
		{
			"permission":"FILL_FIELDS_AND_SIGN",
			"firstName":first_name,
			"lastName":last_name,
			"emailId":email,
			"sequence":1
		}
		],
		"senderEmail":"raymond_camden@foxitsoftware.com"
	}

	headers = {
		'Authorization': f'Bearer {token}',
	}

	response = requests.request("POST", url, headers=headers, json=body)
	return response.json()

Make note that parties is an array of one person, as only one person is needed in the process. Also note that permission is required as it defines the role that person will play in the party.

Calling this method is simple:

# Hard coded template id
tid = "392230"

sendForSigningResponse = sendForSigning(tid, "Raymond", "Camden", "raymondcamden@gmail.com", access_token)
The result of this call is a large set of data. But for now, we can live with just the ID of the envelope created:
envelopeId = sendForSigningResponse['folder']['folderId']
print(f"ID of the envelope created: {envelopeId}")

Note: You'll see 'folder' referenced in the API endpoints and results, but the eSign API is migrating to the 'envelope' term instead.

A few seconds after running this code, the email showed up in my account:

Email in regards to signing

Obviously at this point, I could sign it… but what if I didn't?

Sending out Electronic Reminders

One way to help ensure your important documents get signed is to remind the people who need to sign that – well, they need actually sign the document. To enable this, we can use the Send Signature Reminder endpoint. All it needs is the ID of the envelope created earlier (and again, see my note about envelope vs folder):

def sendReminder(envelope_id, token):	
	url = "https://na1.foxitesign.foxit.com/api/folders/signaturereminder"
	body = {
		"folderId":envelope_id
	}
	headers = {
		'Authorization': f'Bearer {token}',
	}

	response = requests.request("POST", url, headers=headers, json=body)
	result = response.json()
	return result
Calling this just required the access token and ID:
access_token = getAccessToken(CLIENT_ID, CLIENT_SECRET)
result = sendReminder(envelope_id, access_token)
As you can expect, this sends an email reminder:
Email reminder

Ok, But Did They Sign Their Document Yet??

So far, you've seen an example of sending out a document for signing as well as politely reminding the person to do their job. How can you tell if they've completed the process? For this, we can turn to the Get Envelope Details endpoint. It's also rather simple in that it just needs the envelope ID value from before. Once again, here's a wrapper to that API built in Python:

def getStatus(envelope_id, token):	
	url = f"https://na1.foxitesign.foxit.com/api/folders/myfolder?folderId={envelope_id}"

	headers = {
		'Authorization': f'Bearer {token}',
	}

	response = requests.request("GET", url, headers=headers)
	result = response.json()
	return result
And the call:
result = getStatus(envelope_id, access_token)

print(f"Envelope status: {result['folder']['folderStatus']}")

While there's a lot of important information returned, we can output just the status to see at a high level what state the process is currently in.

Given the example I've shown so far, the status of the envelope is SHARED. Let's actually click the link:

Document prepared for signing

In the screenshot above, notice how the date is already filled to today's data. Also note that name was prefilled as eSign knows who it was sent to. All I need to do is click and sign. Once I do, the same code above will now return EXECUTED.

Next Steps

Wondering where to go next? If you are completely new to eSign, check out the main homepage for an introduction. You can also check out the Foxit eSign YouTube channel for lots of good video content on the service. If you want a copy of the code I showed in this post, you can find all three examples here on our GitHub repo. Finally, don't forget to visit our forums and bring your questions!

How to Chain PDF Actions with Foxit

How to Chain PDF Actions with Foxit

Performing a single action with the Foxit PDF Services API is straightforward, but what’s the best way to handle a sequence of operations? Instead of downloading and re-uploading a file for each step, you can chain actions together by passing the output of one job as the input for the next. This tutorial walks you through a complete Python example of how to build an efficient document optimization workflow that compresses and then linearizes a PDF.

How to Chain PDF Actions with Foxit

When working with Foxit’s PDF Services, you’ll remember that the basic flow involves:

  • Uploading your document to Foxit to get an ID
  • Starting a job
  • Checking the job
  • Downloading the result

This is handy for one off operations, for example, converting a Word document to PDF, but what if you need to do two or more operations? Luckily this is easy enough by simply handing off one result to the next. Let’s take a look at how this can work.

Credentials

Remember, to start developing and testing with the APIs, you’ll need to head over to our developer portal and grab a set of free credentials. This will include a client ID and secret values you’ll need to make use of the API.

If you would rather watch a video (or why not both?) – you can watch the walkthrough below:

Creating a Document Optimization Workflow

To demonstrate how to chain different operations together, we’re going to build a basic document optimization workflow that will:

  • Compress the document by reducing image resolution and other compression algorithims.
  • Linearize the document to make it better viewable on the web.

Given the basic flow described above, you may be tempted to do this:

  • Upload the PDF
  • Kick off the Compress job
  • Check until done
  • Download the compressed PDF
  • Upload the PDF
  • Kick off the Linearize job
  • Check until done
  • Download the compressed and linearized PDF

This wouldn’t require much code, but we can simplify the process by using the result of the compress job—once it’s complete—as the source for the linearize job. This gives us the following streamlined flow:

  • Upload the PDF
  • Kick off the Compress job
  • Check until done
  • Kick off the Linearize job
  • Check until done
  • Download the compressed and linearized PDF

Less is better! Alright, let’s look at the code.

First, here’s the typical code used to bring in our credentials from the environment, and define the Upload job:

import os
import requests
import sys 
from time import sleep 

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

def uploadDoc(path, id, secret):
	
	headers = {
		"client_id":id,
		"client_secret":secret
	}

	with open(path, 'rb') as f:
		files = {'file': f}

		request = requests.post(f"{HOST}/pdf-services/api/documents/upload", files=files, headers=headers)
		return request.json()
Next, here are two utility methods to wrap calling Compress and Linearize:
def compressPDF(doc, level, id, secret):
	
	headers = {
		"client_id":id,
		"client_secret":secret,
		"Content-Type":"application/json"
	}

	body = {
		"documentId":doc,
		"compressionLevel":level	
	}

	request = requests.post(f"{HOST}/pdf-services/api/documents/modify/pdf-compress", json=body, headers=headers)
	return request.json()

def linearizePDF(doc, id, secret):

	headers = {
		"client_id":id,
		"client_secret":secret,
		"Content-Type":"application/json"
	}

	body = {
		"documentId":doc
	}

	request = requests.post(f"{HOST}/pdf-services/api/documents/optimize/pdf-linearize", json=body, headers=headers)
	return request.json()

Note that the compressPDF method takes a required level argument that defines the level of compression. From the docs, we can see the supported values are LOW, MEDIUM, and HIGH.

Now, two more utility methods – one that checks the task returned by the API operations above and one that downloads a result to the file system:

def checkTask(task, id, secret):

	headers = {
		"client_id":id,
		"client_secret":secret,
		"Content-Type":"application/json"
	}

	done = False
	while done is False:

		request = requests.get(f"{HOST}/pdf-services/api/tasks/{task}", headers=headers)
		status = request.json()
		if status["status"] == "COMPLETED":
			done = True
			# really only need resultDocumentId, will address later
			return status
		elif status["status"] == "FAILED":
			print("Failure. Here is the last status:")
			print(status)
			sys.exit()
		else:
			print(f"Current status, {status['status']}, percentage: {status['progress']}")
			sleep(5)

def downloadResult(doc, path, id, secret):
	
	headers = {
		"client_id":id,
		"client_secret":secret
	}

	with open(path, "wb") as output:
		
		bits = requests.get(f"{HOST}/pdf-services/api/documents/{doc}/download", stream=True, headers=headers).content 
		output.write(bits)
Alright, so that’s all the utility methods and setup. Time to actually do what we said we would:
input = "../../inputfiles/input.pdf"
print(f"File size of input: {os.path.getsize(input)}")
doc = uploadDoc(input, CLIENT_ID, CLIENT_SECRET)
print(f"Uploaded doc to Foxit, id is {doc['documentId']}")

task = compressPDF(doc["documentId"], "HIGH", CLIENT_ID, CLIENT_SECRET)
print(f"Created task, id is {task['taskId']}")

result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)
print("Done converting to PDF. Now doing linearize.")

task = linearizePDF(result["resultDocumentId"], CLIENT_ID, CLIENT_SECRET)
print(f"Created task, id is {task['taskId']}")

result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)
print("Done with linearize task.")

output = "../../output/really_optimized.pdf"
downloadResult(result["resultDocumentId"], output , CLIENT_ID, CLIENT_SECRET)
print(f"Done and saved to: {output}.")
print(f"File size of output: {os.path.getsize(output)}") 

This code matches the flow described above, with the exception of outputting the size as a handy way to see the result of the compression call. When run, the initial size is 355994 bytes and the final size is 16733. That's a great saving! You should, however, ensure the result matches the quality you desire and if not, consider reducing the level of compression. Linearize doesn't impact the file size, but as stated above will make it work nicer on the web.

For a complete listing, find the sample on our GitHub repo.

Next Steps

Obviously, you could do even more chaining based on the code above. For example, as part of your optimization flow, you could even split the PDF to return a 'sample' of a document that may be for sale. You could extract information to use for AI purposes and more. Dig more into our PDF Service APIs to get an idea and let us know what you build on our developer forums!

How to Extract Text from PDFs using Foxit’s REST APIs

How to Extract Text from PDFs using Foxit's REST APIs

Want to extract text from PDF files with just a few lines of Python? This guide shows how to use Foxit’s REST Extract API to pull text content from PDFs, ideal for search, automation, or AI workflows. From setting up credentials to searching for keywords across multiple files, this post walks through the full process with example code and GitHub demos.

How to Extract Text from PDFs using Foxit’s REST APIs

PDFs are an excellent way to store information—they combine text, images, and more in a perfectly laid-out, eye-catching design that fulfills every marketer’s wildest dreams. But sometimes you just need the text! There’s a variety of reasons you may want to convert a rich PDF document into plain text:

  • For indexing in a search engine
  • To search documents for keywords
  • To pass to generative AI services for introspection

Let’s take a look at the Extract API to see just how easy this is.

Start Here: Obtain Free Credentials to Use the Foxit API

Before we go any further, head over to our developer portal and grab a set of free credentials. This will include a client ID and secret values – you’ll need both to make use of the API.

Rather watch the movie version? Check out the video below:

Foxit PDF API Workflow Overview with Python

The API follows the same format as the rest of our PDF Services in that you upload your input, kick off the job, check the job’s status, and download the result. As we’ve covered this a few times now on the blog (see my introductory post, we’ll skip over the details of uploading the document and loading in credentials. Here’s the Python code we’ve demonstrated before showing this in action:

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

def uploadDoc(path, id, secret):
	
	headers = {
		"client_id":id,
		"client_secret":secret
	}

	with open(path, 'rb') as f:
		files = {'file': (path, f)}

		request = requests.post(f"{HOST}/pdf-services/api/documents/upload", files=files, headers=headers)
		return request.json()

doc = uploadDoc("../../inputfiles/input.pdf", CLIENT_ID, CLIENT_SECRET)
print(f"Uploaded pdf to Foxit, id is {doc['documentId']}")

Now let's get into the meat of the Extract API. The API takes three arguments:

  • The ID of the previously uploaded document.
  • The type of information to extract—either TEXT, IMAGE, or PAGE. In theory, it should be pretty obvious what these do, but just in case: TEXT returns the text contents of the PDF. IMAGE gives you a ZIP file of images from the PDF. PAGE returns a new PDF containing just the page you requested.
  • You can also pass in a page range, which can be a combo of specific pages and ranges. If you don’t include one, the entire PDF gets processed for extraction.

To make this simple to use, I've built a wrapper function that lets you pass these arguments:

def extractPDF(doc, type, id, secret, pageRange=None):
    
    headers = {
        "client_id":id,
        "client_secret":secret,
        "Content-Type":"application/json"
    }

    body = {
        "documentId":doc,
        "extractType":type
    }

    if pageRange:
        body["pageRange"] = pageRange 

    request = requests.post(f"{HOST}/pdf-services/api/documents/modify/pdf-extract", json=body, headers=headers)
    return request.json()

Literally, that's it. At this point, you get a task object back that – like with our other APIs – can be checked for completion, and once it’s done, the results can be downloaded. Since we're working with text, though, let's simplify and just grab the text as a variable:

def getResult(doc, id, secret):
    
    headers = {
        "client_id":id,
        "client_secret":secret
    }

    return requests.get(f"{HOST}/pdf-services/api/documents/{doc}/download", headers=headers).text
This utility method takes a document ID value and gets the textual content. Here’s how that code looks:
doc = uploadDoc("../../inputfiles/input.pdf", CLIENT_ID, CLIENT_SECRET)
print(f"Uploaded pdf to Foxit, id is {doc['documentId']}")

task = extractPDF(doc["documentId"], "TEXT", CLIENT_ID, CLIENT_SECRET)
print(f"Created task, id is {task['taskId']}")

result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)
print(f"Final result: {result}")

text = getResult(result["resultDocumentId"], CLIENT_ID, CLIENT_SECRET)
print(text)
You can see the entire script on our GitHub. Running it will just give you a wall of text. Not terribly exciting. So, let’s make it exciting!

Searching PDFs for Keywords

Let’s iterate on the previous example for something that could be a bit more useful – given a set of input PDFs, extract the text from each and report if a certain keyword, or keywords are found. I’ll start by gathering a list of PDFs from a source directory. But you could imagine this coming from new files in a cloud storage provider, attachments in new emails, and so forth:
# Get PDFs from our input directory
inputFiles = list(filter(lambda x: x.endswith('.pdf'), os.listdir('../../inputfiles')))
Now, I’ll define a keyword. A more complex version of this would probably use a list of keywords, but we’ll keep it simple for now:
# Keyword to match on: 
keyword = "Shakespeare"
And now to actually do the work. Remember, we’ve already defined our methods, so the only thing changing here is the code calling them:
for file in inputFiles:
    
    doc = uploadDoc(f"../../inputfiles/{file}", CLIENT_ID, CLIENT_SECRET)
    print(f"Uploaded pdf, {file}, to Foxit, id is {doc['documentId']}")

    task = extractPDF(doc["documentId"], "TEXT", CLIENT_ID, CLIENT_SECRET)
    result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)

    text = getResult(result["resultDocumentId"], CLIENT_ID, CLIENT_SECRET)
    if keyword in text:
        print(f"\033[32mThe pdf, {file}, matched on our keyword: {keyword}\033[0m")
    else:
        print(f"The pdf, {file}, did not match on our keyword: {keyword}")
    
    print("")
Given my set of inputs, there’s only one match. Here’s the output I received:
Output from the script showing documents that contained the keyword | Foxit APIs
You can find the complete source code for this on our GitHub repo.

What’s Next?

The demo here is fairly simple, but you could imagine it being expanded to include things like automatic routing of PDFs with matching keywords, email alerts, and so forth. As a reminder, when working with any process like this, you can cache the result of the extraction. Imagine a scenario where the important keywords may change in the future. Your code could store the result of the text extract to the file system (perhaps with the same name as the PDF but using `.txt` as the extension instead) and simply skip calling our API when the cache exists. Our API will miss you, but that’s ok.

If this all sounds exciting, be sure to check the docs for more information about the template language and API. Sign up for some free developer credentials and reach out on our developer forums with any questions.

Generate Dynamic PDFs from JSON using Foxit APIs

Generate Dynamic PDFs from JSON using Foxit APIs

See how easy it is to generate PDFs from JSON using Foxit’s Document Generation API. With Word as your template engine, you can dynamically build invoices, offer letters, and agreements—no complex setup required. This tutorial walks through the full process in Python and highlights the flexibility of token-based document creation.

Generate Dynamic PDFs from JSON using Foxit APIs

One of the more fascinating APIs in our library is the Document Generation API. This document generation API lets you create dynamic PDFs or Word documents using your own data as templates. That may sound simple – and the code you’re about to see is indeed simple – but the real power lies in how flexible Word can be as a template engine. This API could be used for:

  • Creating invoices
  • Creating offer letters
  • Creating dynamic agreements (which can integrate with our eSign API)

All of this is made available via a simple API and a “token language” you’ll use within Word to create your templates. Let’s take a look at how this is done.

Credentials

Before we go any further, head over to our developer portal and grab a set of free credentials. This will include a client ID and secret values – you’ll need both to make use of the API.

Don’t want to read all of this? You can also follow along by video:

Using the API

The Document Generation API flow is a bit different from our PDF Services APIs in that the execution is synchronous. You don’t need to upload your document beforehand or download a result result. You simply call the API (passing your data and template) and the result has your new PDF (or Word document). With it being this simple, let’s get into the code.

Loading Credentials

My script begins by loading in the credentials and API root host via the environment:

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

As always, try to avoid hard coding credentials directly into your code.

Calling the API

The endpoint only requires you to pass the output format, your data, and a base64 version of your file. “Your data” can be almost anything you like—though it should start as an object (i.e., a dictionary in Python with key/value pairs). Beneath that, anything goes: strings, numbers, arrays of objects, and so on.

Here’s a Python wrapper showing this in action:

def docGen(doc, data, id, secret):
    
    headers = {
        "client_id":id,
        "client_secret":secret
    }

    body = {
        "outputFormat":"pdf",
        "documentValues": data,  
        "base64FileString":doc
    }

    request = requests.post(f"{HOST}/document-generation/api/GenerateDocumentBase64", json=body, headers=headers)
    return request.json()

And here’s an example calling it:

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

data = {
    "name":"Raymond Camden", 
    "food": "sushi",
    "favoriteMovie": "Star Wars",
    "cats": [
        {"name":"Elise", "gender":"female", "age":14 },
        {"name":"Luna", "gender":"female", "age":13 },
        {"name":"Crackers", "gender":"male", "age":13 },
        {"name":"Gracie", "gender":"female", "age":12 },
        {"name":"Pig", "gender":"female", "age":10 },
        {"name":"Zelda", "gender":"female", "age":2 },
        {"name":"Wednesday", "gender":"female", "age":1 },
    ],
}

result = docGen(b64, data, CLIENT_ID, CLIENT_SECRET)

You’ll note here that my data is hard-coded. In a real application, this would typically be dynamic—read from the file system, queried from a database, or sourced from any other location.

The result object contains a message representing the success or failure of the operation, the file extension for the result, and the base64 representation of the result. Most likely you’ll always be outputting PDFs, so here’s a simple bit of code that stores the result:

with open('../../output/docgen1.pdf', 'wb') as file:
    file.write(binary_data)
    print('Done and stored to ../../output/docgen1.pdf')

There’s a bit more to the API than I’ve shown here so be sure to check the docs, but now it’s time for the real star of this API, Word

Using Word as a Template

I’ve probably used Microsoft Word for longer than you’ve been alive and I’ve never really thought much about it. But when you begin to think of a simple Word document as a template, all of a sudden the possibilities begin to excite you. In our Document Generation API, the template system works via simple “tokens” in your document marked by opening and closing double brackets.

Consider this block of text:

See how name is surrounded by double brackets? And food and favoriteMovie? When this template is sent to the API along with the corresponding values, those tokens are replaced dynamically. In the screenshot, notice how favoriteMovie is bolded. That’s fine. You can use any formatting, styling, or layout options you wish.

That’s one example, but you also get some built-in values as well. For example, including today as a token will insert the current date, and can be paired with date formatting to specify how the date looks:

Remember the array of cats from earlier? You can use that to create a table in Word like this:

Notice that I’ve used two new tags here, TableStart and TableEnd, both of which reference the array, cats. Then in my table cells, I refer to the values from that array. Again, the color you see here is completely arbitrary and was me making use of the entirety of my Word design skills.

Here’s the template as a whole to show you everything in context:

The Result

Given the code shown above with those values, and given the Word template just shared, once passed to the API, the following PDF is created:

Ready to Try?

If this looks cool, be sure to check the docs for more information about the template language and API. Sign up for some free developer credentials and reach out on our developer forums with any questions.

Want the code? Get it on GitHub.

If you are more of a Node person, check out that version. Get it on GitHub.

Introducing PDF APIs from Foxit

Introducing PDF APIs from Foxit

Get started with Foxit’s new PDF APIs—convert Word to PDF, generate documents, and embed files using simple, scalable REST APIs. Includes sample Python code and walkthrough.

Introducing PDF APIs from Foxit

At the end of June, Foxit introduced a brand-new suite of tools to help developers work with documents. These APIs cover a wide range of features, including:

    • Convert between Office document formats and PDF files seamlessly
    • Optimize, manipulate, and secure PDFs with advanced APIs
    • Generate dynamic documents using Microsoft Word templates
    • Extract text and images from PDFs with powerful tools
    • Embed PDFs into web pages in a context-aware, controlled manner
    • Integrate with eSign APIs for streamlined signature workflows


These APIs are simple to use, and best of all, follow the “don’t surprise me” principal of development. In this post, I’m going to demonstrate one simple example – converting a Word document to PDF – but you can rest assured that nearly all the APIs will follow incredibly similar patterns. I’ll be using Python for my examples here, but will link to a Node.js version of the same example. And given that we’re talking REST APIs here, any language is welcome to join the document party. Let’s dive in.

Credentials

Before we go any further, head over to our developer portal and grab a set of free credentials. This will include a client ID and secret values you’ll need to make use of the API.

Don’t want to read all of this? You can also follow along by video:

API Flow

As I mentioned above, most of the PDF Services APIs will follow a similar flow. This comes down to:

  • Upload your input (like a Word document)
  • Kick off a job (like converting to PDF)
  • Check the job (hey, how ya doin?)
  • Download the result

Or, in pretty graphical format –

The great thing is, once you’ve completed one integration (this post focuses on converting Word to PDF), switching to another is easy—and much of your existing code can be reused. A lazy developer is happy developer! Let’s get started.

Loading Credentials

My script begins by loading the credentials and API root host via the environment:

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

It’s never a good idea to hard-code credentials in your code. But if you do it this one time, I won’t tell. Honest.

Uploading Your Input

As I mentioned, in this example we’ll be making use of the Word to PDF API. Our input will be a Word document, which we’ll upload to Foxit using the upload API. This endpoint is fairly simple – aside from your credentials, all you need to provide is the binary data of the input file. Here’s the method I created to make this process easier:

def uploadDoc(path, id, secret):
    
    headers = {
        "client_id":id,
        "client_secret":secret
    }

    with open(path, 'rb') as f:
        files = {'file': (path, f)}

        request = requests.post(f"{HOST}/pdf-services/api/documents/upload", files=files, headers=headers)
        return request.json()

And here’s how it’s used:

doc = uploadDoc("../../inputfiles/input.docx", CLIENT_ID, CLIENT_SECRET)
print(f"Uploaded doc to Foxit, id is {doc['documentId']}")

The upload API only returns one value, a documentId, which we can use in future calls.

Starting the Job

Each API operation is a job creator. By this I mean you call the endpoint and it begins your action. For Word to PDF, the only required input is the document ID from the previous call. We can build a nice little wrapper function like so:

def convertToPDF(doc, id, secret):
    
    headers = {
        "client_id":id,
        "client_secret":secret,
        "Content-Type":"application/json"
    }

    body = {
        "documentId":doc	
    }

    request = requests.post(f"{HOST}/pdf-services/api/documents/create/pdf-from-word", json=body, headers=headers)
    return request.json()

And then call it like so:

task = convertToPDF(doc["documentId"], CLIENT_ID, CLIENT_SECRET)
print(f"Created task, id is {task['taskId']}")

The result of this call, if no errors were found, isa taskId. We can use this to gauge how the job’s performing. Let’s do that now.

Job Checking

Ok, so the next part can be a bit tricky depending on your language of choice. We need to use the task status endpoint to determine how the job is performing. How often we do this, how quickly and so forth, will depend on your platform and needs. For our little sample script here, everything is running at once. I wrote a function that will check the status. If the job isn’t finished (whether successful or not), it pauses briefly before trying again. While this approach isn’t the most sophisticated, it should work well enough for basic testing:

def checkTask(task, id, secret):

    headers = {
        "client_id":id,
        "client_secret":secret,
        "Content-Type":"application/json"
    }

    done = False
    while done is False:

        request = requests.get(f"{HOST}/pdf-services/api/tasks/{task}", headers=headers)
        status = request.json()
        if status["status"] == "COMPLETED":
            done = True
            # really only need resultDocumentId, will address later
            return status
        elif status["status"] == "FAILED":
            print("Failure. Here is the last status:")
            print(status)
            sys.exit()
        else:
            print(f"Current status, {status['status']}, percentage: {status['progress']}")
            sleep(5)

As you can see, I’m using a while loop that—at least in theory—will continue running until a success or failure response is returned, with a five-second pause between each call. You can adjust that interval as needed—test different values to see what works best for your use case. Typically, most API calls should complete in under ten seconds, so a five-second delay felt like a reasonable default.

Each call to the endpoint returns a task status result. Here’s an example:

{
    'taskId': '685abc95a0d113558e4204d7', 
    'status': 'COMPLETED', 
    'progress': 100, 
    'resultDocumentId': '685abc952475582770d6917b'
}

The important part here is the status. But you could also use progress to give some feedback to the code waiting for results. Here’s my code calling this:

result = checkTask(task["taskId"], CLIENT_ID, CLIENT_SECRET)
print(f"Final result: {result}")

Downloading Your Result

The last piece of the puzzle is simply saving the result. If you noticed above, the task returned a resultDocumentId value. Taking that, and the [Download Document](NEED LINK) endpoint, we can build a utility to store the result like so:

def downloadResult(doc, path, id, secret):
    
    headers = {
        "client_id":id,
        "client_secret":secret
    }

    with open(path, "wb") as output:
        
        bits = requests.get(f"{HOST}/pdf-services/api/documents/{doc}/download", stream=True, headers=headers).content 
        output.write(bits)

And finally, call it:

downloadResult(result["resultDocumentId"], "../../output/input.pdf", CLIENT_ID, CLIENT_SECRET)
print("Done and saved to: ../../output/input.pdf")

And that’s it! While this script could certainly benefit from more robust error handling, it demonstrates the basic flow. As mentioned, most of our APIs follow this same logic.

Next Steps

Want the complete scripts? Get it on GitHub.

Want it in Node.js? Get it on GitHub.

Rather try this yourself? Sign up for a free developer account now. Need help? Head over to our developer forums and post your questions and comments.