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 account — free 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
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()
A few things worth noting:
-
api_version="v1"works for FileMaker Server 17-19. For FileMaker Server 2023+, use"vLatest". -
verify_ssl=Trueis the default and you should keep it. If you're hitting self-signed certs in development, passverify_ssl=Falsetemporarily — 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")
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}")
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']}")
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}
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
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")
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()
Run it:
export FM_PASSWORD="your_fm_password"
export PDFFORGE_API_KEY="pk_live_your_api_key"
python generate_invoices.py
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
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
)
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
)
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
-
Install the dependencies:
pip install python-fmrest requests -
Test your FileMaker connection with a simple
find()call - Sign up at pdfforge.dev and upload a template
- Start with one document type — get it working end-to-end before expanding
- 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)