DEV Community

Zero Lopp Labs
Zero Lopp Labs

Posted on

How to Generate PDFs from FileMaker Data Using Python

You already use python-fmrest to pull data from FileMaker. Now what?

Maybe you're exporting records for a report. Maybe you're building invoices at month-end. Maybe you just need to turn FileMaker data into something you can email to a client without sending them a .fmp12 file.

This tutorial shows you how to build a clean pipeline: python-fmrest to fetch your FileMaker data, PDFForge to turn it into professional PDFs. No wkhtmltopdf. No Puppeteer. No LaTeX. Just Python, JSON, and an API call.

What you'll need

  • Python 3.6+ (python-fmrest requirement)
  • A FileMaker Server with the Data API enabled (FileMaker Server 17+ or Claris FileMaker Server)
  • A PDFForge accountfree tier gives you 25 documents/month, no credit card required
  • A PDF or DOCX template uploaded to your PDFForge dashboard

Install the dependencies:

pip install python-fmrest requests
Enter fullscreen mode Exit fullscreen mode

Step 1: Connect to FileMaker with python-fmrest

If you've used python-fmrest before, this is familiar. If not — it's a clean Python wrapper around the FileMaker Data API that handles authentication, sessions, and response parsing for you.

import fmrest

# Connect to your FileMaker Server
fms = fmrest.Server(
    url="https://your-filemaker-server.com",
    user="api_user",
    password="your_password",
    database="Invoicing",
    layout="Invoices",
    api_version="v1",
    verify_ssl=True
)

# Log in — python-fmrest handles the token for you
fms.login()
Enter fullscreen mode Exit fullscreen mode

A few things worth noting:

  • api_version="v1" works for FileMaker Server 17-19. For FileMaker Server 2023+, use "vLatest".
  • verify_ssl=True is the default and you should keep it. If you're hitting self-signed certs in development, pass verify_ssl=False temporarily — but fix that before production.
  • The session token is managed automatically. python-fmrest requests a token on login() and includes it in subsequent requests. It also handles token expiry.

Step 2: Fetch the records you need

Let's say you want to generate invoices for all unpaid orders. python-fmrest's find() method maps directly to the FileMaker Data API's find endpoint:

# Find all unpaid invoices
find_query = [{"Status": "Unpaid"}]
unpaid_invoices = fms.find(query=find_query)

print(f"Found {len(unpaid_invoices)} unpaid invoices")
Enter fullscreen mode Exit fullscreen mode

The result is an iterable of Record objects. Each record gives you attribute-style access to your FileMaker fields:

for invoice in unpaid_invoices:
    print(f"Invoice #{invoice.InvoiceNumber}{invoice.CustomerName} — ${invoice.Total}")
Enter fullscreen mode Exit fullscreen mode

Getting related records (line items)

For invoices, you typically need data from a related table — line items, for example. python-fmrest supports portals:

# Get a specific invoice with portal data
# Make sure your layout includes a portal to InvoiceLines
record = fms.get_record(42, portals=[{"name": "InvoiceLines"}])

# Access portal records — python-fmrest stores portals as record attributes
# with a "portal_" prefix: record.portal_<PortalName>
# Convert to list since Foundset is a lazy iterator
line_items = list(record.portal_InvoiceLines)
for line in line_items:
    print(f"  {line['InvoiceLines::Description']} — qty {line['InvoiceLines::Quantity']}")
Enter fullscreen mode Exit fullscreen mode

Portal field names follow FileMaker's convention: TableOccurrence::FieldName.

Step 3: Generate a PDF with PDFForge

Now the interesting part. You have your FileMaker data in Python objects. Let's turn it into a PDF.

PDFForge works with templates — you upload a PDF form, a DOCX file with placeholders, or an HTML template to your dashboard, and then fill it with data via the API. For this tutorial, we'll use a DOCX invoice template.

Upload a template (one-time setup)

In your PDFForge dashboard, upload a DOCX file with {placeholder} tags:

INVOICE #{invoice_number}
Date: {date}
Due: {due_date}

Bill to:
{customer_name}
{customer_address}

{items_table}

Subtotal: {subtotal}
VAT ({tax_rate}%): {tax_amount}
Total: {total} {currency}
Enter fullscreen mode Exit fullscreen mode

After uploading, you'll get a template_id (like tpl_abc123). Save that — you'll need it in the code.

Make the API call

import requests

PDFFORGE_API_KEY = "pk_live_your_api_key_here"  # Use env vars in production
TEMPLATE_ID = "tpl_your_template_id"

def generate_invoice_pdf(invoice, line_items):
    """Generate a PDF from a FileMaker invoice record."""

    # Build the data payload from the FileMaker record
    payload = {
        "template_id": TEMPLATE_ID,
        "data": {
            "invoice_number": str(invoice.InvoiceNumber),
            "date": str(invoice.InvoiceDate),
            "due_date": str(invoice.DueDate),
            "customer_name": invoice.CustomerName,
            "customer_address": invoice.CustomerAddress,
            "items": [
                {
                    "description": item["InvoiceLines::Description"],
                    "quantity": item["InvoiceLines::Quantity"],
                    "unit_price": item["InvoiceLines::UnitPrice"],
                    "amount": item["InvoiceLines::LineTotal"]
                }
                for item in line_items
            ],
            "subtotal": invoice.Subtotal,
            "tax_rate": invoice.TaxRate,
            "tax_amount": invoice.TaxAmount,
            "total": invoice.GrandTotal,
            "currency": invoice.Currency
        }
    }

    # Call PDFForge API
    response = requests.post(
        "https://api.pdfforge.dev/v1/documents/fill",
        headers={
            "Authorization": f"Bearer {PDFFORGE_API_KEY}",
            "Content-Type": "application/json"
        },
        json=payload,
        timeout=30
    )
    response.raise_for_status()

    result = response.json()
    return result
Enter fullscreen mode Exit fullscreen mode

The response includes a download_url where you can fetch the generated document:

result = generate_invoice_pdf(invoice, line_items)

# Download the PDF
pdf_response = requests.get(result["download_url"])
with open(f"Invoice_{invoice.InvoiceNumber}.pdf", "wb") as f:
    f.write(pdf_response.content)

print(f"Saved Invoice_{invoice.InvoiceNumber}.pdf")
Enter fullscreen mode Exit fullscreen mode

Step 4: Put it all together

Here's the complete pipeline — connect, fetch, generate, save:

import fmrest
import requests
import os

# -- Configuration --
FM_SERVER = "https://your-filemaker-server.com"
FM_USER = "api_user"
FM_PASSWORD = os.environ["FM_PASSWORD"]  # Never hardcode passwords
FM_DATABASE = "Invoicing"

PDFFORGE_API_KEY = os.environ["PDFFORGE_API_KEY"]
TEMPLATE_ID = "tpl_your_template_id"
OUTPUT_DIR = "./invoices"

def main():
    # Connect to FileMaker
    fms = fmrest.Server(
        url=FM_SERVER,
        user=FM_USER,
        password=FM_PASSWORD,
        database=FM_DATABASE,
        layout="InvoicesWithPortal",
        api_version="v1"
    )
    fms.login()

    # Find unpaid invoices
    unpaid = fms.find(query=[{"Status": "Unpaid"}])
    print(f"Found {len(unpaid)} unpaid invoices")

    os.makedirs(OUTPUT_DIR, exist_ok=True)
    generated = 0
    errors = 0

    for invoice in unpaid:
        try:
            # Get portal data for line items
            record = fms.get_record(
                invoice.record_id,
                portals=[{"name": "InvoiceLines"}]
            )
            line_items = list(record.portal_InvoiceLines)

            if not line_items:
                print(f"  Skipping Invoice #{invoice.InvoiceNumber} — no line items")
                continue

            # Generate PDF
            result = generate_invoice_pdf(record, line_items)

            # Download and save
            pdf_response = requests.get(result["download_url"])
            filepath = os.path.join(OUTPUT_DIR, f"Invoice_{invoice.InvoiceNumber}.pdf")
            with open(filepath, "wb") as f:
                f.write(pdf_response.content)

            print(f"  Generated: {filepath}")
            generated += 1

        except requests.exceptions.HTTPError as e:
            print(f"  ERROR Invoice #{invoice.InvoiceNumber}: {e}")
            errors += 1
        except Exception as e:
            print(f"  ERROR Invoice #{invoice.InvoiceNumber}: {e}")
            errors += 1

    # Clean up FileMaker session
    fms.logout()

    print(f"\nDone. Generated: {generated}, Errors: {errors}")


def generate_invoice_pdf(invoice, line_items):
    """Generate a PDF from a FileMaker invoice record."""
    payload = {
        "template_id": TEMPLATE_ID,
        "data": {
            "invoice_number": str(invoice.InvoiceNumber),
            "date": str(invoice.InvoiceDate),
            "due_date": str(invoice.DueDate),
            "customer_name": invoice.CustomerName,
            "customer_address": invoice.CustomerAddress,
            "items": [
                {
                    "description": item["InvoiceLines::Description"],
                    "quantity": item["InvoiceLines::Quantity"],
                    "unit_price": item["InvoiceLines::UnitPrice"],
                    "amount": item["InvoiceLines::LineTotal"]
                }
                for item in line_items
            ],
            "subtotal": invoice.Subtotal,
            "tax_rate": invoice.TaxRate,
            "tax_amount": invoice.TaxAmount,
            "total": invoice.GrandTotal,
            "currency": invoice.Currency
        }
    }

    response = requests.post(
        "https://api.pdfforge.dev/v1/documents/fill",
        headers={
            "Authorization": f"Bearer {PDFFORGE_API_KEY}",
            "Content-Type": "application/json"
        },
        json=payload,
        timeout=30
    )
    response.raise_for_status()
    return response.json()


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it:

export FM_PASSWORD="your_fm_password"
export PDFFORGE_API_KEY="pk_live_your_api_key"
python generate_invoices.py
Enter fullscreen mode Exit fullscreen mode
Found 12 unpaid invoices
  Generated: ./invoices/Invoice_2026-0031.pdf
  Generated: ./invoices/Invoice_2026-0032.pdf
  Skipping Invoice #2026-0033 — no line items
  Generated: ./invoices/Invoice_2026-0034.pdf
  ...
Done. Generated: 11, Errors: 0
Enter fullscreen mode Exit fullscreen mode

Beyond invoices: other template types

The same pattern works for any document type. Only the template and data shape change.

HTML templates (for complex layouts)

If your documents need advanced formatting — tables that span pages, conditional sections, dynamic charts — use an HTML template with Handlebars syntax. Upload it via the API or dashboard, then call /v1/documents/generate instead of /fill:

# For HTML templates, use the /generate endpoint
response = requests.post(
    "https://api.pdfforge.dev/v1/documents/generate",
    headers={
        "Authorization": f"Bearer {PDFFORGE_API_KEY}",
        "Content-Type": "application/json"
    },
    json={
        "template_id": "tpl_html_report_template",
        "data": {
            "title": "Monthly Sales Report",
            "period": "March 2026",
            "records": [
                {"product": record.ProductName, "revenue": record.Revenue}
                for record in sales_records
            ]
        }
    },
    timeout=30
)
Enter fullscreen mode Exit fullscreen mode

PDF form filling

Got existing PDF forms with AcroForm fields? Upload the PDF, and PDFForge maps your JSON keys to the form field names:

# Fill a PDF form — same /fill endpoint, different template
response = requests.post(
    "https://api.pdfforge.dev/v1/documents/fill",
    headers={
        "Authorization": f"Bearer {PDFFORGE_API_KEY}",
        "Content-Type": "application/json"
    },
    json={
        "template_id": "tpl_application_form",
        "data": {
            "applicant_name": record.FullName,
            "date_of_birth": str(record.DOB),
            "address": record.Address,
            "signature_date": "2026-03-23"
        }
    },
    timeout=30
)
Enter fullscreen mode Exit fullscreen mode

What this costs

python-fmrest is open source and free.

PDFForge has a free tier at 25 documents/month — enough to build and test your pipeline. If you need more:

Plan Price Documents/month
Free $0 25
Starter $29/mo 500
Pro $79/mo 2,500
Business $199/mo 10,000

For a typical FileMaker solution generating monthly invoices, the free tier or Starter plan covers most scenarios. Compare that to per-seat plugin licenses that can run $200+ per developer.

Current pricing is always at pdfforge.dev.

The honest trade-offs

No tool is perfect. Here's what you should consider:

  • Network dependency. Every PDF generation requires an API call. If your environment has no internet access, this approach won't work.
  • Latency. An API call adds 1-3 seconds per document compared to local generation. For batch jobs running overnight, this doesn't matter. For real-time, click-and-wait UX, it's noticeable.
  • Data leaves your server. Your document data is sent to PDFForge's servers for rendering. PDFForge retains documents temporarily (7 days on free tier) then deletes them. If your data is highly sensitive, review their security practices or consider whether this fits your compliance requirements.
  • FileMaker Data API limits. The Data API has its own rate limits and concurrent session caps. If you're processing thousands of records, you may need to batch your find() calls.

Next steps

  1. Install the dependencies: pip install python-fmrest requests
  2. Test your FileMaker connection with a simple find() call
  3. Sign up at pdfforge.dev and upload a template
  4. Start with one document type — get it working end-to-end before expanding
  5. Use environment variables for all credentials (never hardcode API keys or passwords)

The pipeline is simple by design: fetch data, shape it as JSON, send it to an API, get back a document. Once you have this pattern, you can extend it to any document your FileMaker solution needs — contracts, reports, shipping labels, compliance forms.

If you're already using python-fmrest, you're one pip install requests away from professional PDF generation.


PDFForge is a REST API for document generation. python-fmrest is an open-source Python wrapper for the FileMaker Data API by David Hamann.

Top comments (0)