Back to Resources
October 29, 2025 By Zack Stevenson

Auto-Generate PDFs from Airtable with PDFMonkey

A step-by-step guide to automating PDF generation (invoices, packing slips, and more) directly from your Airtable base using PDFMonkey — no external automation tool required.

If your business generates invoices, packing slips, proposals, or any kind of document from structured data, you can fully automate that process using Airtable and PDFMonkey — with no external automation tool required. This guide walks through the complete integration so you can go from manual copy-paste to one-click PDF generation directly inside Airtable.

What You’ll Build

By the end of this tutorial you’ll have a system that:

  • Triggers PDF generation with a button click in your Airtable interface
  • Populates a professionally designed template with live data from your base (including line items)
  • Stores the generated PDF attachment directly on the Airtable record
  • Uses PDFMonkey’s synchronous API — no polling or waiting required

Video Walkthrough


Prerequisites

  • An Airtable accounta paid plan is required for the “Run script” automation action
  • A PDFMonkey account — a free tier is available
  • Basic comfort with HTML and JavaScript (most of the code can be copied directly from this guide)
  • Your PDFMonkey API key

Tools Required

ToolPurpose
AirtableYour data source and automation host
PDFMonkeyPDF template engine and generation API

No Make, Zapier, or n8n needed. Everything runs inside Airtable’s automation platform.


Step 1: Set Up Your Airtable Base

Your Airtable base needs two tables with the following structure.

Invoices Table

FieldType
Invoice NumberFormula: {ID} + 1000
Customer NameSingle line text
Customer AddressSingle line text
Invoice DateDate
Due DateDate
StatusSingle select
Total AmountRollup (sum of Line Items)
NotesLong text
Invoice PDFAttachment
Line ItemsLink to “Line Items” table
IDAuto number

Line Items Table

FieldType
IDAuto number
InvoiceLink to “Invoices” table
Item DescriptionSingle line text
QuantityNumber
Unit PriceCurrency
Total Line AmountFormula: {Quantity} * {Unit Price}

Note: Field names are case-sensitive and must match exactly when you configure the automation script. If you use different names, update them in the script accordingly.


Step 2: Create Your PDFMonkey Template

Sign up at pdfmonkey.io and create a new Document Template. You’ll use HTML and CSS to design the invoice layout, and PDFMonkey’s Liquid-style syntax for dynamic fields.

After creating your template, you’ll need two pieces of information:

  • Your API Key — available at https://dashboard.pdfmonkey.io/api-key
  • Your Template ID — found in the template URL (the UUID at the end) or via the “Copy ID” button in the editor

PDFMonkey template ID location

Invoice HTML Template

Here is a complete, production-ready invoice template you can paste into PDFMonkey’s HTML editor:

<head>
    <title>Invoice</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Helvetica', 'Arial', sans-serif;
            font-size: 12px;
            line-height: 1.6;
            color: #333;
            padding: 40px;
        }
        .invoice-container { max-width: 800px; margin: 0 auto; }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 40px;
            padding-bottom: 20px;
            border-bottom: 3px solid #2c3e50;
        }
        .company-name { font-size: 28px; font-weight: bold; color: #2c3e50; margin-bottom: 5px; }
        .company-details { font-size: 11px; color: #666; line-height: 1.4; }
        .invoice-title { text-align: right; }
        .invoice-title h1 { font-size: 36px; color: #2c3e50; margin-bottom: 10px; }
        .invoice-number { font-size: 14px; color: #666; }
        .invoice-details {
            display: flex;
            justify-content: space-between;
            margin-bottom: 40px;
        }
        .section-title {
            font-size: 11px;
            font-weight: bold;
            color: #999;
            text-transform: uppercase;
            margin-bottom: 10px;
            letter-spacing: 0.5px;
        }
        .customer-name { font-size: 16px; font-weight: bold; color: #2c3e50; margin-bottom: 5px; }
        .invoice-meta { text-align: right; }
        .meta-row { display: flex; justify-content: flex-end; margin-bottom: 8px; }
        .meta-label { font-weight: bold; margin-right: 15px; color: #666; }
        .meta-value { color: #333; min-width: 120px; text-align: right; }
        .line-items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
        .line-items-table thead { background-color: #2c3e50; color: white; }
        .line-items-table th {
            padding: 12px;
            text-align: left;
            font-size: 11px;
            font-weight: bold;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .line-items-table th.text-right,
        .line-items-table td.text-right { text-align: right; }
        .line-items-table th.text-center,
        .line-items-table td.text-center { text-align: center; }
        .line-items-table tbody tr { border-bottom: 1px solid #e0e0e0; }
        .line-items-table tbody tr:last-child { border-bottom: 2px solid #2c3e50; }
        .line-items-table td { padding: 12px; font-size: 12px; }
        .totals-section { display: flex; justify-content: flex-end; margin-bottom: 40px; }
        .totals-table { width: 300px; }
        .total-row {
            display: flex;
            justify-content: space-between;
            padding: 10px 0;
            font-size: 14px;
        }
        .total-row.grand-total {
            border-top: 2px solid #2c3e50;
            font-weight: bold;
            font-size: 18px;
            color: #2c3e50;
            margin-top: 10px;
            padding-top: 15px;
        }
        .notes-section {
            margin-top: 40px;
            padding: 20px;
            background-color: #f9f9f9;
            border-left: 4px solid #2c3e50;
        }
        .footer {
            margin-top: 60px;
            padding-top: 20px;
            border-top: 1px solid #e0e0e0;
            text-align: center;
            font-size: 10px;
            color: #999;
        }
    </style>
</head>
<body>
    <div class="invoice-container">
        <div class="header">
            <div class="company-info">
                <div class="company-name">Your Company Name</div>
                <div class="company-details">
                    123 Main Street<br>
                    City, State 00000<br>
                    billing@yourcompany.com
                </div>
            </div>
            <div class="invoice-title">
                <h1>INVOICE</h1>
                <div class="invoice-number">#{{invoiceNumber}}</div>
            </div>
        </div>
        <div class="invoice-details">
            <div class="bill-to">
                <div class="section-title">Bill To</div>
                <div class="customer-name">{{customerName}}</div>
                <div class="customer-address">{{customerAddress}}</div>
            </div>
            <div class="invoice-meta">
                <div class="meta-row">
                    <span class="meta-label">Invoice Date:</span>
                    <span class="meta-value">{{invoiceDate}}</span>
                </div>
                <div class="meta-row">
                    <span class="meta-label">Due Date:</span>
                    <span class="meta-value">{{dueDate}}</span>
                </div>
            </div>
        </div>
        <table class="line-items-table">
            <thead>
                <tr>
                    <th>Description</th>
                    <th class="text-center">Qty</th>
                    <th class="text-right">Unit Price</th>
                    <th class="text-right">Amount</th>
                </tr>
            </thead>
            <tbody>
                {% for item in lineItems %}
                <tr>
                    <td>{{item.description}}</td>
                    <td class="text-center">{{item.quantity}}</td>
                    <td class="text-right">${{item.unitPrice}}</td>
                    <td class="text-right">${{item.totalAmount}}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        <div class="totals-section">
            <div class="totals-table">
                <div class="total-row grand-total">
                    <span>Total:</span>
                    <span>${{totalAmount}}</span>
                </div>
            </div>
        </div>
        {% if notes %}
        <div class="notes-section">
            <div class="notes-title">Notes</div>
            <div class="notes-content">{{notes}}</div>
        </div>
        {% endif %}
        <div class="footer">Thank you for your business!</div>
    </div>
</body>

Test Data

Before connecting to Airtable, verify your template renders correctly. In the PDFMonkey template editor, go to the Test Data tab and paste this JSON:

{
  "invoiceNumber": "INV-2025-001",
  "customerName": "Acme Corporation",
  "customerAddress": "456 Business Blvd, Suite 200\nNew York, NY 10001",
  "invoiceDate": "October 28, 2025",
  "dueDate": "November 27, 2025",
  "totalAmount": "3,450.00",
  "notes": "Payment due within 30 days. Include invoice number with payment.",
  "lineItems": [
    { "description": "Website Development", "quantity": "1", "unitPrice": "2,500.00", "totalAmount": "2,500.00" },
    { "description": "SEO Optimization", "quantity": "1", "unitPrice": "750.00", "totalAmount": "750.00" },
    { "description": "Monthly Hosting", "quantity": "2", "unitPrice": "100.00", "totalAmount": "200.00" }
  ]
}

Step 3: Create the Airtable Automation

In your Airtable base, go to Automations and create a new automation.

Automation Setup

  1. Trigger: “When a button is clicked” — add a Button field to your Invoices table and connect it to this automation
  2. Action: Click ”+ Add action” and choose “Run script”
  3. Input variables: Click ”+ Add input variable” and add:
    • pdfMonkeyApiKey (Secret) — your PDFMonkey API key
    • record (Record) — the “record id” from the trigger

Airtable input variables setup

The Automation Script

Copy and paste this script into the Airtable script editor. The only required change is swapping in your Template ID on the line marked below.

// ============================================
// AIRTABLE TO PDFMONKEY — INVOICE GENERATOR
// ============================================
// Uses PDFMonkey's synchronous endpoint: no polling needed.
// Author: Zack Stevenson @ InnerDev Solutions
// Docs: https://docs.pdfmonkey.io/pdfmonkey-features/synchronous-generation

// ============================================
// CONFIGURATION — update these values
// ============================================

let apiKey = input.secret("pdfMonkeyApiKey");

// Your Template ID from the PDFMonkey template URL or "Copy ID" button
let templateId = "YOUR-TEMPLATE-ID-HERE"; // <- Update this

const INVOICES_TABLE_NAME = "Invoices";       // <- Update if different
const LINE_ITEMS_TABLE_NAME = "Line Items";   // <- Update if different

let invoicesTable = base.getTable(INVOICES_TABLE_NAME);
let lineItemsTable = base.getTable(LINE_ITEMS_TABLE_NAME);

// ============================================
// RETRIEVE INVOICE RECORD
// ============================================

let inputConfig = input.config();
const recordId = inputConfig.record;

console.log("Starting invoice generation for record:", recordId);

let invoiceQuery = await invoicesTable.selectRecordsAsync();
let record = invoiceQuery.getRecord(recordId);

if (!record) {
    output.set('status', 'error');
    throw new Error(`Record ${recordId} not found`);
}

// ============================================
// EXTRACT INVOICE DATA
// ============================================

// Update field names below if yours differ
let invoiceData = {
    invoiceNumber: record.getCellValue("Invoice Number"),
    customerName:  record.getCellValue("Customer Name"),
    customerAddress: record.getCellValue("Customer Address"),
    invoiceDate:   record.getCellValue("Invoice Date"),
    dueDate:       record.getCellValue("Due Date"),
    totalAmount:   record.getCellValue("Total Amount"),
    notes:         record.getCellValue("Notes")
};

// Format dates: "October 29, 2025"
const formatDate = (val) => val
    ? new Date(val).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
    : null;

invoiceData.invoiceDate = formatDate(invoiceData.invoiceDate);
invoiceData.dueDate     = formatDate(invoiceData.dueDate);

if (invoiceData.totalAmount) {
    invoiceData.totalAmount = invoiceData.totalAmount.toFixed(2);
}

// ============================================
// EXTRACT LINE ITEMS
// ============================================

let lineItemsRecords = record.getCellValue("Line Items");
let lineItems = [];

if (lineItemsRecords && lineItemsRecords.length > 0) {
    let lineItemsQuery = await lineItemsTable.selectRecordsAsync();

    for (let linked of lineItemsRecords) {
        let li = lineItemsQuery.getRecord(linked.id);
        if (li) {
            let qty   = li.getCellValue("Quantity");
            let price = li.getCellValue("Unit Price");
            let total = li.getCellValue("Total Line Amount");
            lineItems.push({
                description: li.getCellValue("Item Description"),
                quantity:    qty.toFixed(0),
                unitPrice:   price.toFixed(2),
                totalAmount: total.toFixed(2)
            });
        }
    }
}

// ============================================
// CALL PDFMONKEY SYNCHRONOUS API
// ============================================

let payload = {
    invoiceNumber:   invoiceData.invoiceNumber,
    customerName:    invoiceData.customerName,
    customerAddress: invoiceData.customerAddress,
    invoiceDate:     invoiceData.invoiceDate,
    dueDate:         invoiceData.dueDate,
    totalAmount:     invoiceData.totalAmount,
    notes:           invoiceData.notes,
    lineItems:       lineItems
};

let meta = {
    _filename: `Invoice-${invoiceData.invoiceNumber}.pdf`,
    airtableRecordId: record.id,
    generatedAt: new Date().toISOString()
};

try {
    let response = await fetch('https://api.pdfmonkey.io/api/v1/documents/sync', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            document: {
                document_template_id: templateId,
                status: "pending",
                payload: payload,
                meta: meta
            }
        })
    });

    let data = await response.json();

    if (response.ok) {
        let doc = data.document_card;

        if (doc.status === 'success') {
            console.log("PDF generated:", doc.download_url);
            output.set('status', 'success');
            output.set('downloadUrl', doc.download_url);
            output.set('filename', doc.filename);
        } else {
            console.error("Generation failed:", doc.failure_cause);
            output.set('status', 'failure');
            output.set('error', doc.failure_cause);
        }
    } else {
        console.error("API error:", response.status, JSON.stringify(data));
        output.set('status', 'error');
        output.set('error', data);
    }

} catch (error) {
    console.error("Script error:", error.message);
    output.set('status', 'error');
    output.set('error', error.message);
}

Pro tip: The script uses PDFMonkey’s synchronous endpoint (/api/v1/documents/sync). It waits for generation to complete before responding, so you get the download URL immediately — no polling loop needed.


Step 4: Save the PDF Back to the Record

After the “Run script” action succeeds, add an “Update record” action:

  1. Click ”+ Add advanced logic or action” and choose “Update record”
  2. Set Table to “Invoices”
  3. Set Record ID to the record ID from Step 1 (the trigger)
  4. Map the Invoice PDF field to downloadUrl from the script output

Airtable update record configuration

This saves the generated PDF as an attachment on the record so it’s accessible directly from Airtable.


Troubleshooting Common Issues

PDF generates with blank fields Check that your payload key names match the variable names in your PDFMonkey template exactly. These are case-sensitive (invoiceNumber vs invoice_number are different).

Automation fails with an API error Verify your pdfMonkeyApiKey secret is correct and your Template ID matches what’s in the PDFMonkey dashboard URL.

Line items show as raw text Ensure your template uses {% for item in lineItems %} iteration and that lineItems is a JSON array, not a stringified value.

Automation triggers but “Invoice PDF” field stays empty Make sure the “Update record” action is mapped to the downloadUrl output variable from the script — not status or another variable.

Dates show as null in the PDF Airtable returns date values in ISO format. The formatDate helper in the script handles this conversion. If you’re pulling dates differently, check the raw value with console.log first.


What to Build Next

Once this base automation is running, common extensions include:

  • Multi-template routing: use different PDFMonkey templates based on document type (invoice vs. quote vs. packing slip) by adding a conditional before the API call
  • Email delivery: add a “Send email” action after the update step to deliver the PDF link to the client automatically
  • Automatic invoice numbering: use Airtable Automations to increment a counter field and populate the Invoice Number before triggering this script
  • Client portal: store the public_share_link output on the record and surface it in a client-facing Airtable interface

Need help setting this up for your specific workflow? Get in touch. We build these integrations regularly and can typically have a working system in place within a few days.

Ready to put this into practice?

We build these exact kinds of systems for businesses every day. Let's talk about what you need.

Schedule a Free Consultation