PDF Generation from Notion, Airtable, and Third-Party APIs: Automate Everything š
October 6, 2025
Your workflow looks like this:
- Update data in Notion
- Copy data to spreadsheet
- Format data for PDF
- Generate PDF manually
- 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:
- Export data from Salesforce ā 5 minutes
- Format in Excel ā 10 minutes
- Copy to PDF template ā 15 minutes
- Generate PDF ā 2 minutes
- 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/clientGet Notion API Key
- Go to https://www.notion.so/my-integrations
- Create new integration
- Copy "Internal Integration Token"
- 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 airtableFetch 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 googleapisAuthenticate 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:
- Trigger: New row in Google Sheets
- Action: Webhook POST to your endpoint
- 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.