reportgen
Back to all posts

PDF Generation from Notion, Airtable, and Third-Party APIs: Automate Everything šŸ”—

October 6, 2025

Your workflow looks like this:

  1. Update data in Notion
  2. Copy data to spreadsheet
  3. Format data for PDF
  4. Generate PDF manually
  5. Repeat every week

There has to be a better way.

Spoiler: There is.

You can automatically pull data from Notion, Airtable, Google Sheets, Stripe, or any API - and generate PDFs on autopilot.

This guide shows you exactly how - with real integrations, working code, and production-ready examples.


Why Integrate Third-Party APIs?

The Manual Problem

Scenario: You generate weekly sales reports.

Current process:

  1. Export data from Salesforce → 5 minutes
  2. Format in Excel → 10 minutes
  3. Copy to PDF template → 15 minutes
  4. Generate PDF → 2 minutes
  5. Email to stakeholders → 3 minutes

Total time: 35 minutes per week Ɨ 52 weeks = 30+ hours per year

The Automated Solution

// Run this once per week (automated)
async function generateWeeklySalesReport() {
  // Pull data from Salesforce
  const salesData = await fetchFromSalesforce();
 
  // Generate PDF
  const pdf = await generatePDF('sales-report', salesData);
 
  // Email stakeholders
  await emailPDF(pdf, stakeholders);
}
 
// Runs every Monday at 9 AM
schedule.scheduleJob('0 9 * * 1', generateWeeklySalesReport);

Total time: 0 minutes (runs automatically)


Integration 1: Notion Database → PDF

Use Case: Client Proposals

You maintain client data in Notion. Generate proposals automatically.

Setup Notion API

npm install @notionhq/client

Get Notion API Key

  1. Go to https://www.notion.so/my-integrations
  2. Create new integration
  3. Copy "Internal Integration Token"
  4. Share your database with the integration

Fetch Data from Notion

import { Client } from '@notionhq/client';
 
const notion = new Client({
  auth: process.env.NOTION_API_KEY
});
 
async function getClientsFromNotion(databaseId) {
  const response = await notion.databases.query({
    database_id: databaseId,
    filter: {
      property: 'Status',
      select: {
        equals: 'Active'
      }
    },
    sorts: [
      {
        property: 'Name',
        direction: 'ascending'
      }
    ]
  });
 
  // Transform Notion data to usable format
  const clients = response.results.map(page => ({
    id: page.id,
    name: page.properties.Name.title[0]?.plain_text || '',
    email: page.properties.Email.email || '',
    company: page.properties.Company.rich_text[0]?.plain_text || '',
    project_scope: page.properties['Project Scope'].rich_text[0]?.plain_text || '',
    budget: page.properties.Budget.number || 0,
    start_date: page.properties['Start Date'].date?.start || null
  }));
 
  return clients;
}
 
// Usage
const clients = await getClientsFromNotion('your-database-id');

Generate Proposal PDF

async function generateClientProposal(clientId) {
  // Fetch client data from Notion
  const clients = await getClientsFromNotion(process.env.NOTION_DATABASE_ID);
  const client = clients.find(c => c.id === clientId);
 
  if (!client) {
    throw new Error('Client not found');
  }
 
  // Build proposal template
  const html = `
<!DOCTYPE html>
<html>
<head>
  <title>Proposal for ${client.company}</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    h1 { color: #2563eb; }
    .section { margin: 30px 0; }
    .budget { font-size: 24px; font-weight: bold; color: #10b981; }
  </style>
</head>
<body>
  <h1>Project Proposal</h1>
 
  <div class="section">
    <h2>Client Information</h2>
    <p><strong>Company:</strong> ${client.company}</p>
    <p><strong>Contact:</strong> ${client.name}</p>
    <p><strong>Email:</strong> ${client.email}</p>
  </div>
 
  <div class="section">
    <h2>Project Scope</h2>
    <p>${client.project_scope}</p>
  </div>
 
  <div class="section">
    <h2>Investment</h2>
    <p class="budget">$${client.budget.toLocaleString()}</p>
  </div>
 
  <div class="section">
    <h2>Timeline</h2>
    <p><strong>Start Date:</strong> ${new Date(client.start_date).toLocaleDateString()}</p>
  </div>
</body>
</html>
  `;
 
  // Generate PDF
  const pdf = await generatePDF(html);
 
  return pdf;
}

Automated Notion → PDF Workflow

// Monitor Notion for new proposals
async function monitorNotionForProposals() {
  const response = await notion.databases.query({
    database_id: process.env.NOTION_DATABASE_ID,
    filter: {
      and: [
        {
          property: 'Generate Proposal',
          checkbox: { equals: true }
        },
        {
          property: 'Proposal Generated',
          checkbox: { equals: false }
        }
      ]
    }
  });
 
  for (const page of response.results) {
    try {
      // Generate proposal
      const pdf = await generateClientProposal(page.id);
 
      // Upload PDF
      const pdfUrl = await uploadToS3(pdf, `proposals/${page.id}.pdf`);
 
      // Update Notion with PDF link
      await notion.pages.update({
        page_id: page.id,
        properties: {
          'Proposal Generated': { checkbox: true },
          'Proposal URL': { url: pdfUrl }
        }
      });
 
      console.log(`Generated proposal for ${page.id}`);
    } catch (err) {
      console.error(`Failed to generate proposal for ${page.id}:`, err);
    }
  }
}
 
// Run every 5 minutes
setInterval(monitorNotionForProposals, 5 * 60 * 1000);

Integration 2: Airtable → PDF Invoices

Setup Airtable

npm install airtable

Fetch Invoices from Airtable

import Airtable from 'airtable';
 
const base = new Airtable({
  apiKey: process.env.AIRTABLE_API_KEY
}).base(process.env.AIRTABLE_BASE_ID);
 
async function getPendingInvoices() {
  const invoices = [];
 
  await base('Invoices')
    .select({
      filterByFormula: '{Status} = "Pending"',
      sort: [{ field: 'Invoice Date', direction: 'desc' }]
    })
    .eachPage((records, fetchNextPage) => {
      records.forEach(record => {
        invoices.push({
          id: record.id,
          invoice_number: record.get('Invoice Number'),
          customer_name: record.get('Customer Name'),
          customer_email: record.get('Customer Email'),
          invoice_date: record.get('Invoice Date'),
          due_date: record.get('Due Date'),
          line_items: record.get('Line Items'), // Linked records
          total: record.get('Total'),
          status: record.get('Status')
        });
      });
 
      fetchNextPage();
    });
 
  return invoices;
}

Generate Invoice PDF with Line Items

async function generateInvoiceFromAirtable(invoiceId) {
  // Fetch invoice
  const invoice = await base('Invoices').find(invoiceId);
 
  // Fetch line items (linked records)
  const lineItemIds = invoice.get('Line Items') || [];
  const lineItems = await Promise.all(
    lineItemIds.map(id => base('Line Items').find(id))
  );
 
  const invoiceData = {
    invoice_number: invoice.get('Invoice Number'),
    customer_name: invoice.get('Customer Name'),
    customer_email: invoice.get('Customer Email'),
    invoice_date: new Date(invoice.get('Invoice Date')).toLocaleDateString(),
    due_date: new Date(invoice.get('Due Date')).toLocaleDateString(),
    items: lineItems.map(item => ({
      description: item.get('Description'),
      quantity: item.get('Quantity'),
      price: item.get('Unit Price'),
      total: item.get('Quantity') * item.get('Unit Price')
    })),
    total: invoice.get('Total')
  };
 
  const html = `
<!DOCTYPE html>
<html>
<head>
  <title>Invoice ${invoiceData.invoice_number}</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
    th { background: #f0f0f0; }
    .total { font-size: 20px; font-weight: bold; }
  </style>
</head>
<body>
  <h1>Invoice ${invoiceData.invoice_number}</h1>
 
  <p><strong>Customer:</strong> ${invoiceData.customer_name}</p>
  <p><strong>Email:</strong> ${invoiceData.customer_email}</p>
  <p><strong>Date:</strong> ${invoiceData.invoice_date}</p>
  <p><strong>Due Date:</strong> ${invoiceData.due_date}</p>
 
  <table>
    <thead>
      <tr>
        <th>Description</th>
        <th>Quantity</th>
        <th>Price</th>
        <th>Total</th>
      </tr>
    </thead>
    <tbody>
      ${invoiceData.items.map(item => `
      <tr>
        <td>${item.description}</td>
        <td>${item.quantity}</td>
        <td>$${item.price.toFixed(2)}</td>
        <td>$${item.total.toFixed(2)}</td>
      </tr>
      `).join('')}
    </tbody>
    <tfoot>
      <tr>
        <td colspan="3" class="total">Total</td>
        <td class="total">$${invoiceData.total.toFixed(2)}</td>
      </tr>
    </tfoot>
  </table>
</body>
</html>
  `;
 
  const pdf = await generatePDF(html);
 
  return { pdf, invoiceData };
}

Automated Airtable → PDF → Email

async function processAirtableInvoices() {
  const pendingInvoices = await getPendingInvoices();
 
  for (const invoice of pendingInvoices) {
    try {
      // Generate PDF
      const { pdf, invoiceData } = await generateInvoiceFromAirtable(invoice.id);
 
      // Upload to S3
      const pdfUrl = await uploadToS3(pdf, `invoices/${invoice.invoice_number}.pdf`);
 
      // Send email
      await sendEmail({
        to: invoiceData.customer_email,
        subject: `Invoice ${invoiceData.invoice_number}`,
        body: `Please find attached invoice ${invoiceData.invoice_number}.`,
        attachments: [{ filename: `invoice-${invoiceData.invoice_number}.pdf`, content: pdf }]
      });
 
      // Update Airtable
      await base('Invoices').update(invoice.id, {
        'Status': 'Sent',
        'PDF URL': pdfUrl,
        'Sent Date': new Date().toISOString()
      });
 
      console.log(`Sent invoice ${invoice.invoice_number}`);
    } catch (err) {
      console.error(`Failed to process invoice ${invoice.id}:`, err);
    }
  }
}
 
// Run every hour
setInterval(processAirtableInvoices, 60 * 60 * 1000);

Integration 3: Google Sheets → PDF Reports

Setup Google Sheets API

npm install googleapis

Authenticate with Service Account

import { google } from 'googleapis';
 
const auth = new google.auth.GoogleAuth({
  keyFile: 'path/to/service-account-key.json',
  scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
});
 
const sheets = google.sheets({ version: 'v4', auth });

Fetch Data from Sheet

async function getDataFromSheet(spreadsheetId, range) {
  const response = await sheets.spreadsheets.values.get({
    spreadsheetId,
    range
  });
 
  const rows = response.data.values;
 
  if (!rows || rows.length === 0) {
    return [];
  }
 
  // Convert to objects (first row as headers)
  const headers = rows[0];
  const data = rows.slice(1).map(row => {
    const obj = {};
    headers.forEach((header, index) => {
      obj[header] = row[index] || '';
    });
    return obj;
  });
 
  return data;
}
 
// Usage
const salesData = await getDataFromSheet(
  'your-spreadsheet-id',
  'Sheet1!A1:F100'
);

Generate Report from Sheet Data

async function generateSalesReportFromSheet() {
  // Fetch sales data
  const salesData = await getDataFromSheet(
    process.env.GOOGLE_SHEET_ID,
    'Sales!A1:E1000'
  );
 
  // Calculate totals
  const totalRevenue = salesData.reduce((sum, row) => sum + parseFloat(row.Revenue || 0), 0);
  const totalSales = salesData.length;
 
  // Generate PDF
  const html = `
<!DOCTYPE html>
<html>
<head>
  <title>Sales Report</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    .summary { background: #f0f0f0; padding: 20px; border-radius: 8px; margin: 20px 0; }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
    th { background: #2563eb; color: white; }
  </style>
</head>
<body>
  <h1>Sales Report</h1>
 
  <div class="summary">
    <h2>Summary</h2>
    <p><strong>Total Sales:</strong> ${totalSales}</p>
    <p><strong>Total Revenue:</strong> $${totalRevenue.toLocaleString()}</p>
    <p><strong>Average Sale:</strong> $${(totalRevenue / totalSales).toFixed(2)}</p>
  </div>
 
  <h2>Sales Details</h2>
  <table>
    <thead>
      <tr>
        <th>Date</th>
        <th>Customer</th>
        <th>Product</th>
        <th>Quantity</th>
        <th>Revenue</th>
      </tr>
    </thead>
    <tbody>
      ${salesData.slice(0, 100).map(row => `
      <tr>
        <td>${row.Date}</td>
        <td>${row.Customer}</td>
        <td>${row.Product}</td>
        <td>${row.Quantity}</td>
        <td>$${parseFloat(row.Revenue || 0).toFixed(2)}</td>
      </tr>
      `).join('')}
    </tbody>
  </table>
</body>
</html>
  `;
 
  const pdf = await generatePDF(html);
 
  return pdf;
}

Integration 4: Stripe → PDF Receipts

Fetch Stripe Charges

import Stripe from 'stripe';
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
 
async function generateReceiptForCharge(chargeId) {
  // Fetch charge
  const charge = await stripe.charges.retrieve(chargeId, {
    expand: ['customer', 'invoice']
  });
 
  const receiptData = {
    receipt_number: charge.id,
    date: new Date(charge.created * 1000).toLocaleDateString(),
    customer_name: charge.customer?.name || charge.billing_details.name,
    customer_email: charge.customer?.email || charge.receipt_email,
    amount: charge.amount / 100, // Convert from cents
    currency: charge.currency.toUpperCase(),
    payment_method: charge.payment_method_details.card.brand.toUpperCase(),
    last4: charge.payment_method_details.card.last4,
    description: charge.description || 'Payment'
  };
 
  const html = `
<!DOCTYPE html>
<html>
<head>
  <title>Receipt ${receiptData.receipt_number}</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; max-width: 600px; margin: 0 auto; }
    h1 { color: #635bff; }
    .receipt-box { border: 2px solid #e0e0e0; padding: 30px; border-radius: 8px; }
    .amount { font-size: 36px; font-weight: bold; color: #10b981; }
  </style>
</head>
<body>
  <div class="receipt-box">
    <h1>Payment Receipt</h1>
 
    <p><strong>Receipt Number:</strong> ${receiptData.receipt_number}</p>
    <p><strong>Date:</strong> ${receiptData.date}</p>
 
    <hr>
 
    <p><strong>Customer:</strong> ${receiptData.customer_name}</p>
    <p><strong>Email:</strong> ${receiptData.customer_email}</p>
 
    <hr>
 
    <p><strong>Description:</strong> ${receiptData.description}</p>
 
    <p class="amount">$${receiptData.amount.toFixed(2)} ${receiptData.currency}</p>
 
    <p><strong>Payment Method:</strong> ${receiptData.payment_method} ending in ${receiptData.last4}</p>
 
    <hr>
 
    <p style="font-size: 12px; color: #666;">Thank you for your business!</p>
  </div>
</body>
</html>
  `;
 
  const pdf = await generatePDF(html);
 
  return pdf;
}

Zapier/Make.com Integration

Trigger PDF Generation via Webhook

import express from 'express';
 
const app = express();
 
app.post('/api/generate-pdf', async (req, res) => {
  try {
    const { template, data, email } = req.body;
 
    // Generate PDF
    const pdf = await generatePDF(template, data);
 
    // Upload to S3
    const pdfUrl = await uploadToS3(pdf, `generated/${Date.now()}.pdf`);
 
    // Optionally email
    if (email) {
      await sendEmail({
        to: email,
        subject: 'Your PDF is ready',
        body: `Download: ${pdfUrl}`,
        attachments: [{ filename: 'document.pdf', content: pdf }]
      });
    }
 
    res.json({
      success: true,
      pdf_url: pdfUrl
    });
  } catch (err) {
    res.status(500).json({
      success: false,
      error: err.message
    });
  }
});
 
app.listen(3000);

Use with Zapier:

  1. Trigger: New row in Google Sheets
  2. Action: Webhook POST to your endpoint
  3. Send data from sheet to generate PDF

Best Practices for API Integrations

1. Handle Rate Limits

import pLimit from 'p-limit';
 
const limit = pLimit(5); // Max 5 concurrent requests
 
async function processAllRecords(records) {
  const tasks = records.map(record => limit(async () => {
    return await generatePDFForRecord(record);
  }));
 
  return await Promise.all(tasks);
}

2. Implement Retry Logic

async function fetchWithRetry(fetchFn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetchFn();
    } catch (err) {
      if (i === maxRetries - 1) throw err;
 
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

3. Cache API Responses

import NodeCache from 'node-cache';
 
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
 
async function getCachedData(cacheKey, fetchFn) {
  const cached = cache.get(cacheKey);
  if (cached) return cached;
 
  const data = await fetchFn();
  cache.set(cacheKey, data);
 
  return data;
}

Final Thoughts

Stop manual data entry. Automate everything.

With API integrations, you can:

  • āœ… Pull data from any source
  • āœ… Generate PDFs automatically
  • āœ… Eliminate manual work
  • āœ… Run on autopilot

Your time is valuable. Let the machines do the boring stuff.

Ready to automate PDF generation? Try reportgen.io - integrate with any API in minutes.