DEV Community

Zero Lopp Labs
Zero Lopp Labs

Posted on

How to Generate PDFs from FileMaker with a REST API

FileMaker's built-in PDF generation works — until it doesn't.

You build a layout. You use "Save Records as PDF." It looks fine on your machine. Then a client opens the same file on Windows and the fonts are wrong, the margins shifted, and that carefully aligned logo is now floating somewhere in the header wilderness.

Sound familiar? Let's fix it.

The problem with FileMaker's native PDF generation

FileMaker's "Save Records as PDF" script step is layout-based. That means your PDF output is a screenshot of a layout, not a document built from structured data. This creates a cascade of problems:

Layout dependency. Every PDF format requires its own layout. Need an invoice, a quote, and a packing slip? That's three layouts to maintain, each with pixel-perfect alignment that breaks when you change a field.

Cross-platform inconsistency. FileMaker renders layouts differently on Mac vs. Windows vs. Server. Font substitution, line height differences, and margin calculations vary between platforms. What looks perfect on your MacBook might be a mess on a client's Windows server running a scheduled script.

No batch processing. Want to generate 200 invoices at once? You'll need a loop script that navigates to each record, switches to the right layout, saves the PDF, and moves on. It's slow, fragile, and ties up the FileMaker client while running.

Limited formatting. Conditional formatting exists, but try building a multi-page document with dynamic tables, page breaks at logical points, and different headers per section. You'll spend more time fighting the layout engine than building your solution.

Server-side limitations. FileMaker Server can run "Save Records as PDF," but only with layouts that use server-compatible fonts. Get a font wrong and you get blank pages — with no useful error message.

The API approach: send JSON, get PDF

What if PDF generation worked like this instead?

  1. Your FileMaker script collects the data (customer name, line items, totals — whatever you need)
  2. It sends that data as JSON to an API endpoint
  3. It gets back a ready-to-use PDF file

No layouts to maintain. No platform inconsistencies. The same JSON payload produces the exact same PDF whether you send it from Mac, Windows, FileMaker Server, or a scheduled script.

This is what a REST API for PDF generation does. The template lives on the API side. Your FileMaker solution just provides the data.

Native PDF vs Plugin vs REST API

Before we dive into the tutorial, here's an honest comparison of the three main approaches:

Native "Save as PDF" FileMaker Plugin REST API (PDFForge)
Setup Built-in, zero setup Install plugin on every client + server API key, no install
Template control Layout-based Varies by plugin HTML/CSS or DOCX templates
Cross-platform Inconsistent rendering Plugin must support each OS Identical output everywhere
Batch generation Loop script, slow Depends on plugin Parallel API calls
Server compatibility Font limitations Plugin must be server-compatible No server-side install needed
Cost Free (included) $50-500+ license per seat Free tier, then usage-based
Maintenance Layouts break with schema changes Plugin updates, version locks API versioned, no client updates
Network dependency None None Requires internet connection
Learning curve Familiar Plugin-specific API Standard REST/JSON

The honest trade-off: The API approach adds a network dependency. If your FileMaker solution runs in an environment with no internet access, this won't work. For everything else — which is most modern deployments — the flexibility and consistency gains are significant.

Step-by-step: calling PDFForge from a FileMaker script

Let's build a complete FileMaker script that generates a PDF invoice. We'll use PDFForge, a REST API designed specifically for document generation.

Prerequisites

  • A PDFForge account (free tier available at pdfforge.dev)
  • Your API key (found in your dashboard after signup)
  • FileMaker Pro 16+ or Claris Pro (for Insert from URL with cURL options)

Step 1: Store your API key

Create a table called AppSettings (if you don't have one already) with a field for PDFForge_API_Key. Store your API key there. Never hardcode API keys in scripts.

// In your AppSettings table:
PDFForge_API_Key = "your_api_key_here"
Enter fullscreen mode Exit fullscreen mode

Step 2: Build the JSON payload

FileMaker's JSONSetElement function is your best friend here. Create a script called "Generate Invoice PDF" and start building the payload:

# ============================================
# Script: Generate Invoice PDF
# Purpose: Collect invoice data, call PDFForge API, save PDF
# ============================================

# -- Step 1: Build the JSON payload from current record --

Set Variable [ $json ; Value: "{}" ]

# Company details
Set Variable [ $json ; Value:
  JSONSetElement ( $json ;
    [ "template_id" ; "tpl_YOUR_TEMPLATE_ID" ; JSONString ] ;
        [ "data.company.name" ; AppSettings::CompanyName ; JSONString ] ;
    [ "data.company.address" ; AppSettings::CompanyAddress ; JSONString ] ;
    [ "data.company.email" ; AppSettings::CompanyEmail ; JSONString ] ;
    [ "data.company.phone" ; AppSettings::CompanyPhone ; JSONString ] ;
    [ "data.company.logo_url" ; AppSettings::LogoURL ; JSONString ]
  )
]

# Customer details
Set Variable [ $json ; Value:
  JSONSetElement ( $json ;
    [ "data.customer.name" ; Invoices::CustomerName ; JSONString ] ;
    [ "data.customer.address" ; Invoices::CustomerAddress ; JSONString ] ;
    [ "data.customer.email" ; Invoices::CustomerEmail ; JSONString ]
  )
]

# Invoice metadata
Set Variable [ $json ; Value:
  JSONSetElement ( $json ;
    [ "data.invoice.number" ; Invoices::InvoiceNumber ; JSONString ] ;
    [ "data.invoice.date" ; GetAsText ( Invoices::InvoiceDate ) ; JSONString ] ;
    [ "data.invoice.due_date" ; GetAsText ( Invoices::DueDate ) ; JSONString ] ;
    [ "data.invoice.currency" ; Invoices::Currency ; JSONString ]
  )
]
Enter fullscreen mode Exit fullscreen mode

Step 3: Add line items from a related table

Line items need to be built as a JSON array. Here's how to loop through a portal or related records:

# -- Step 2: Build line items array --

Set Variable [ $items ; Value: "[]" ]
Set Variable [ $i ; Value: 0 ]

Go to Related Record [ From table: "InvoiceLines" ; Using layout: "InvoiceLines" ]

Loop
  Set Variable [ $item ; Value:
    JSONSetElement ( "{}" ;
      [ "description" ; InvoiceLines::Description ; JSONString ] ;
      [ "quantity" ; InvoiceLines::Quantity ; JSONNumber ] ;
      [ "unit_price" ; InvoiceLines::UnitPrice ; JSONNumber ] ;
      [ "amount" ; InvoiceLines::LineTotal ; JSONNumber ]
    )
  ]

  Set Variable [ $items ; Value:
    JSONSetElement ( $items ; $i ; $item ; JSONObject )
  ]

  Set Variable [ $i ; Value: $i + 1 ]

  Go to Record/Request/Page [ Next ; Exit after last ]
End Loop

# Navigate back to invoice layout
Go to Layout [ "Invoices" ; Animation: None ]

# Add line items and totals to payload
Set Variable [ $json ; Value:
  JSONSetElement ( $json ;
    [ "data.items" ; $items ; JSONArray ] ;
    [ "data.totals.subtotal" ; Invoices::Subtotal ; JSONNumber ] ;
    [ "data.totals.tax_rate" ; Invoices::TaxRate ; JSONNumber ] ;
    [ "data.totals.tax_amount" ; Invoices::TaxAmount ; JSONNumber ] ;
    [ "data.totals.total" ; Invoices::GrandTotal ; JSONNumber ]
  )
]
Enter fullscreen mode Exit fullscreen mode

Step 4: Call the API with Insert from URL

Now the actual API call. FileMaker's Insert from URL script step supports cURL options, which gives you full control over the HTTP request:

# -- Step 3: Call PDFForge API --

Set Variable [ $api_key ; Value: AppSettings::PDFForge_API_Key ]

Set Variable [ $curl_options ; Value:
  "--request POST" &
  " --header \"Content-Type: application/json\"" &
  " --header \"Authorization: Bearer pk_live_" & $api_key & "\"" &
  " --data @$json"
]

Set Variable [ $url ; Value: "https://api.pdfforge.dev/v1/documents/generate" ]

Insert from URL [ Select ; With dialog: Off ;
  Target: $pdf_result ;
  URL: $url ;
  cURL options: $curl_options
]
Enter fullscreen mode Exit fullscreen mode

Step 5: Handle the response

The API returns a JSON response containing a download_url. You'll need to parse the JSON, then fetch the PDF from that URL:

# -- Step 4: Handle the response --

Set Variable [ $http_code ; Value: Get ( LastExternalErrorDetail ) ]

If [ $http_code = 0 ]

  # Parse the JSON response to get the download URL
  Set Variable [ $download_url ; Value:
    JSONGetElement ( $pdf_result ; "download_url" )
  ]
  Set Variable [ $doc_id ; Value:
    JSONGetElement ( $pdf_result ; "id" )
  ]

  # Download the actual PDF file
  Insert from URL [ Select ; With dialog: Off ;
    Target: $pdf_binary ;
    URL: $download_url
  ]

  # Save PDF to container field
  Set Field [ Invoices::PDF_Document ; $pdf_binary ]

  # Optionally export to disk
  Set Variable [ $filename ; Value:
    "Invoice_" & Invoices::InvoiceNumber & ".pdf"
  ]
  Set Variable [ $filepath ; Value:
    Get ( DocumentsPath ) & $filename
  ]
  Export Field Contents [ Invoices::PDF_Document ; "$filepath" ]

  Show Custom Dialog [ "Success" ;
    "PDF generated: " & $filename
  ]

Else

  # Error handling
  Show Custom Dialog [ "Error" ;
    "PDF generation failed. HTTP status: " & $http_code &
    "¶Response: " & $pdf_result
  ]

End If
Enter fullscreen mode Exit fullscreen mode

The complete cURL equivalent

If you want to test outside FileMaker first (or you're integrating from another platform), here's the equivalent cURL command:

curl -X POST https://api.pdfforge.dev/v1/documents/generate \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer pk_live_YOUR_API_KEY" \
  -d '{
    "template_id": "tpl_YOUR_TEMPLATE_ID",
    "data": {
      "company": {
        "name": "Acme Corp",
        "address": "123 Business Street, London EC1A 1BB",
        "email": "billing@acme.example.com"
      },
      "customer": {
        "name": "Jane Smith",
        "address": "456 Client Road, Manchester M1 1AA",
        "email": "jane@example.com"
      },
      "invoice": {
        "number": "INV-2026-0042",
        "date": "2026-03-22",
        "due_date": "2026-04-21",
        "currency": "GBP"
      },
      "items": [
        {
          "description": "FileMaker Development - Phase 1",
          "quantity": 40,
          "unit_price": 95.00,
          "amount": 3800.00
        },
        {
          "description": "PDF Template Design",
          "quantity": 1,
          "unit_price": 500.00,
          "amount": 500.00
        }
      ],
      "totals": {
        "subtotal": 4300.00,
        "tax_rate": 20,
        "tax_amount": 860.00,
        "total": 5160.00
      }
    }
  }' \
  }' | jq .
Enter fullscreen mode Exit fullscreen mode

The API returns a JSON response with a download_url — fetch that URL to get the actual PDF file:

# Download the generated PDF
curl -o invoice.pdf "$(curl -s -X POST https://api.pdfforge.dev/v1/documents/fill \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer pk_live_YOUR_API_KEY" \
  -d '...' | jq -r '.download_url')"
Enter fullscreen mode Exit fullscreen mode

Same data, same template, same result, every time.

Error handling and resilience

Production scripts need more than a happy path. Here are the patterns that matter:

Timeout handling

FileMaker's Insert from URL has a default timeout. For large documents, set it explicitly:

Set Variable [ $curl_options ; Value:
  $curl_options & " --max-time 30"
]
Enter fullscreen mode Exit fullscreen mode

Retry logic

Network requests can fail transiently. A simple retry loop helps:

Set Variable [ $max_retries ; Value: 3 ]
Set Variable [ $attempt ; Value: 1 ]

Loop
  Insert from URL [ Select ; With dialog: Off ;
    Target: $pdf_result ;
    URL: $url ;
    cURL options: $curl_options
  ]

  Exit Loop If [ Get ( LastExternalErrorDetail ) = 0 or $attempt >= $max_retries ]

  Set Variable [ $attempt ; Value: $attempt + 1 ]

  # Brief pause before retry
  Pause/Resume Script [ Duration (seconds): 2 ]
End Loop
Enter fullscreen mode Exit fullscreen mode

Validate before sending

Don't waste API calls on incomplete data:

# Pre-flight checks
If [ IsEmpty ( Invoices::CustomerName ) or IsEmpty ( Invoices::InvoiceNumber ) ]
  Show Custom Dialog [ "Missing Data" ;
    "Customer name and invoice number are required."
  ]
  Exit Script [ Text Result: "error" ]
End If
Enter fullscreen mode Exit fullscreen mode

Going further

Once you have the basic pattern working, there's a lot more you can do.

Custom templates

PDFForge supports three template types, each uploaded via the dashboard or API (POST /v1/templates):

  • PDF Forms — AcroForms with fillable fields (text, checkboxes, dropdowns). Use endpoint /v1/documents/fill.
  • DOCX Files — Word documents with {placeholder} tags, including image support. Use endpoint /v1/documents/fill.
  • HTML Templates — Handlebars syntax, rendered to PDF via Chromium. Use endpoint /v1/documents/generate.

Your FileMaker solution sends the same JSON structure — only the template_id changes:

# Switch template by changing the template_id
Set Variable [ $json ; Value:
  JSONSetElement ( $json ;
    [ "template_id" ; "tpl_YOUR_OTHER_TEMPLATE" ; JSONString ]
  )
]
Enter fullscreen mode Exit fullscreen mode

DOCX templates

Need to generate editable Word documents? Upload a DOCX template with {placeholder} tags in the dashboard, then call /v1/documents/fill with your data. PDFForge merges the data into the template and returns the filled document. This is useful when clients need to edit documents before sending.

Batch processing

Need to generate 200 invoices at month-end? Instead of looping through layouts, loop through records and make API calls. Each call is independent — no layout switching, no render context to maintain:

# Batch generation script
Go to Record/Request/Page [ First ]

Loop
  Perform Script [ "Generate Invoice PDF" ]
  Go to Record/Request/Page [ Next ; Exit after last ]
End Loop
Enter fullscreen mode Exit fullscreen mode

Because the API is stateless, you could even run multiple FileMaker scripts in parallel (using Perform Script on Server calls) for faster batch processing.

Email delivery

Combine PDF generation with an email API (like Resend or SendGrid) to generate and send invoices in one automated workflow. Your FileMaker script becomes: collect data, generate PDF, attach to email, send. All without a user clicking through layouts.

What this costs

PDFForge has a free tier (25 documents/month, no credit card required) that's enough to test and build your integration. Paid plans start at $29/month with higher document quotas and extended retention. For most FileMaker solutions, that's a fraction of what you'd pay for a per-seat plugin license.

Check current pricing at pdfforge.dev.

Next steps

  1. Sign up at pdfforge.dev and grab your API key
  2. Test with cURL first — paste the command above into your terminal to see the output
  3. Build the FileMaker script — start with the code in this article and adapt it to your schema
  4. Customize a template — upload your own HTML/CSS or DOCX template in the dashboard
  5. Automate — add the script to your existing workflows (record creation, month-end batch, email triggers)

If you hit any issues or want to share what you've built, find us at pdfforge.dev. We're building this for developers who actually ship FileMaker solutions — your feedback shapes the product.

Top comments (0)