Email-to-Booklet: Parse, Personalize, and Merge Legal PDFs with n8n + PDF.co
What You’ll Have When Done
A flow that:
- Watches an email inbox for messages with legal documents (PDF attachments).
- Uploads each attachment to PDF.co’s temporary storage (so we get stable URLs).
- Runs Document Parser on the filled form to extract:
project_title,client_name,reference_id,generated_at. - Generates a personalized cover page from an HTML Template (hosted in PDF.co) using those extracted values.
- Merges (cover + all uploaded PDFs) into a single booklet.
- Saves the final booklet in Google Drive.
Prerequisites
- PDF.co API Key – sign up: https://app.pdf.co/
- Mailbox with IMAP access (for Email Trigger)
- Google Drive OAuth2 connected in n8n
- A Google Drive folder for the final merged booklet
- PDF.co HTML Template created in editor: https://app.pdf.co/html-templates-tool/editor/new
- Copy your Template ID (you can view/manage templates here: https://app.pdf.co/html-templates-tool/editor)
- PDF.co Document Parser Template created here: https://app.pdf.co/document-parser/templates/new
- Test and copy its Template ID using: https://app.pdf.co/document-parser-tool/playground/choose-template
Quick Start Options
Option A — I want it working now
Important: Install the PDF.co node in n8n before importing the JSON workflow. The blueprint contains five PDF.co community nodes, and they may not appear if the workflow is imported before the node is installed.
In n8n version 1.94.0 or later, open the Nodes panel, search for PDF.co, select the node, and click Install Node.
Alternatively, go to Settings → Community Nodes, search forn8n-nodes-pdfco, and install it.
If you already imported the blueprint and its PDF.co nodes are missing, install the node and import the JSON again. Installation may require workspace-owner or instance-owner access.
- Install the PDF.co community node using the instructions above.
- Download and import your workflow JSON (Link Here).
- Open each node and:
- Set your IMAP credentials in Email Trigger (IMAP).
- Add your PDF.co API credentials to each PDF.co node: PDFco Api, PDFco Api1, PDFco Api2, PDFco Api3, and PDFco Api4.
- In the Document Parser HTTP Request node, add your PDF.co API key to the
x-api-keyheader and enter your Document ParsertemplateId. - In PDFco Api4, enter your HTML Template ID and ensure its
templateDatafields match the keys returned by Document Parser. - Select the destination folder in the Google Drive upload node.
- Run the workflow once with a test email, verify the resulting booklet, and then activate the workflow.
Option B — Build it step-by-step (tutorial below)
Follow the steps. I’ll show exact settings and payloads.
Endpoint Index (you’ll use these)
- PDF Upload : Docs: https://docs.pdf.co/api-reference/file-upload/upload
POST /v1/file/upload/
- Document Parser (extract fields from filled form): Docs: https://docs.pdf.co/api-reference/documentparser/overview
POST /v1/pdf/documentparser
- HTML Template → PDF (cover page): Docs: https://docs.pdf.co/api-reference/pdf-from-html/convert-from-template
POST /v1/pdf/convert/from/html(template mode in the n8n custom node is often labeled URL/HTML to PDF → htmlTemplateToPDF)
- PDF Merge (booklet): Docs: https://docs.pdf.co/api-reference/merge/pdf
POST /v1/pdf/merge
Step-by-Step Build
IN THIS TUTORIAL
Step 1: Email Trigger & attachments
Node: Email Trigger (IMAP)
Goal: Watch for new emails with subject “Legal Documents” and download attachments.
Settings
- Download Attachments: Enabled
Options → customEmailConfig: ["UNSEEN", ["SUBJECT", "Legal Documents"]]
- What happens: Each new matching email becomes one execution, with its attachments available as
binary.attachment_0,binary.attachment_1, etc.
Step 2: Upload each attachment to PDF.co (to obtain stable, public URLs)
Reason: PDF.co cannot download OAuth-protected links or private blobs; you upload binary and get a temporary URL to use downstream (merge, parser, etc.).
You can use your custom PDFco Api nodes (as in your sample), or do it transparently with two HTTP nodes:
- Get Presigned URL (POST)
- PUT Binary to
presignedUrl
If you prefer the custom nodes you already have (PDFco Api, PDFco Api1, PDFco Api2, PDFco Api3) that say “Operation: Upload File to PDF.co” with binaryData = true and binaryPropertyName = attachment_n — perfect. Each of those will output a JSON with the final url.
In your sample, you map:
attachment_0→ Engagement_Agreementattachment_1→ Filled_Formattachment_2→ SOWattachment_3→ MNDA
Each of these must output a json.url field for later.
API Reference (for completeness):
POST https://api.pdf.co/v1/file/upload/ → returns url
Step 3: Parse the filled form to extract client data
Node: HTTP Request (name: HTTP Request)
Endpoint Used: Document Parser Docs: https://docs.pdf.co/api-reference/documentparser/overview
Settings
- Method: POST
- URL:
https://api.pdf.co/v1/pdf/documentparser - Headers:
Content-Type: application/jsonx-api-key: YOUR_API_KEY
Body: JSON
{
"url": "{{ $json.url }}",
"outputFormat": "JSON",
"templateId": "YOUR_TEMPLATE_ID",
"async": false,
"inline": "true"
}- Make sure the item entering this node is the “Filled_Form” upload output (so
{{ $json.url }}points to the filled form’s uploaded URL). Use your realtemplateIdfrom: https://app.pdf.co/document-parser-tool/playground/choose-template
Output shape (simplified):
{
"body": {
"objects": [
{ "name": "project_title", "value": "XYZ Rollout" },
{ "name": "client_name", "value": "Acme Corp" },
{ "name": "reference_id", "value": "REF-4492" },
{ "name": "generated_at", "value": "2025-11-09 13:10" }
]
}
}Step 4: Build a clean filename (optional but recommended)
Node: Code (name: Code1)
Goal: Create a safe fileName (and coverName) for later nodes, using parser output.
Code (copy–paste):
/**
* Build a clean, safe filename from parser output
* Uses body.objects values (prefer by key names)
*/
function sanitize(s = '') {
return String(s)
.trim()
.replace(/[\/\\:*?"<>|]+/g, '')
.replace(/\s+/g, ' ')
.replace(/[\s]+/g, '_')
.replace(/[^\w.-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
}
function pick(objects, keyName, idxFallback = 0) {
if (Array.isArray(objects)) {
const byKey = objects.find(o =>
(o?.name || o?.key || o?.field || '').toString().toLowerCase() === keyName
);
if (byKey?.value) return byKey.value;
return objects[idxFallback]?.value;
}
return undefined;
}
return items.map(item => {
const objects = item.json?.body?.objects || [];
const rawProject = pick(objects, 'project_title', 0) ?? 'Untitled_Project';
const rawClient = pick(objects, 'client_name', 1) ?? 'Unknown_Client';
const project = sanitize(rawProject);
const client = sanitize(rawClient);
const date = new Date().toISOString().slice(0,10);
const base = `${project}_${client}_${date}`.slice(0, 180);
return {
json: {
...item.json,
projectTitle: rawProject,
clientName: rawClient,
fileBase: base,
fileName: `${base}.pdf`,
coverName: `00_Cover_${base}.pdf`
}
};
});Step 5: Generate the cover page from your PDF.co HTML Template
Node: PDFco Api4
Endpoint Used: HTML to PDF (from Template)
Docs: https://docs.pdf.co/api-reference/pdf-from-html/convert-from-template
Settings
- Operation:
URL/HTML to PDF - Convert Type:
htmlTemplateToPDF - templateId:
YOUR_HTML_TEMPLATE_ID- templateData: (this must match the keys your template expects)
{
"project_title": "{{ $json.body.objects[0].value }}",
"client_name": "{{ $json.body.objects[1].value }}",
"reference_id": "{{ $json.body.objects[2].value }}",
"generated_at": "{{ $json.body.objects[3].value }}"
}- If your parser keys are named, use them (e.g.,
{{ $json.body.objects.find(o => o.name==='project_title').value }}) — but the simple index form is fine if your order is stable.
Advanced Options → name: ={{ $json.fileBase }}_Cover
- What happens: PDF.co renders your template with those values → returns a url to the generated cover PDF.
Step 6: Gather all PDF URLs (cover + four uploaded documents)
Node: Merge (mode: Wait for all inputs)
Connect inputs from:
PDFco Api3(Cover page)PDFco Api1(Filled_Form upload → also goes to parser)PDFco Api(Engagement_Agreement upload)PDFco Api2(SOW upload)PDFco Api4(MNDA upload)
Then: a Code node to collect URLs and build a final merged name.
Code (copy–paste):
// Collect URLs, keep first 5, build final name
function sanitizeName(s = '') {
return String(s)
.trim()
.replace(/[\/\\:*?"<>|]+/g, '')
.replace(/\s+/g, ' ')
.replace(/[\s]+/g, '_')
.replace(/[^\w.\-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
}
const incoming = $input.all();
const urls = [];
const nameHints = [];
for (const it of incoming) {
// Most PDFco upload/convert nodes return { url, name }
const u = it.json?.url || it.json?.fileUrl || it.json?.downloadUrl;
if (typeof u === 'string' && /^https?:\/\//i.test(u)) {
urls.push(u);
}
nameHints.push(it.json?.name, it.json?.fileName, it.json?.filename);
}
// keep order, de-dup, cap at 5
const seen = new Set();
const unique = [];
for (const u of urls) {
if (!seen.has(u)) { seen.add(u); unique.push(u); }
if (unique.length >= 5) break;
}
// final merged filename (prefer existing base from upstream if present)
const base = sanitizeName($json.fileBase || 'AttachmentPacket');
const fileName = `${base}.pdf`;
const out = { fileName, urls: unique };
unique.forEach((u, i) => out[`url${i + 1}`] = u);
return [{ json: out }];You’ll now have:
url1..url5fileName(e.g.,AcmeCorp_ProjectX_2025-11-09.pdf)
Step 7: Merge PDFs
Node: PDFco Api5
Endpoint Used: PDF Merge
Docs: https://docs.pdf.co/api-reference/merge/pdf
Settings
- Operation:
Merge PDF - url (multiple): map your ordered list:
={{ $json.urls[1] }} // e.g., Cover
={{ $json.urls[0] }} // Agreement
={{ $json.urls[2] }} // Filled Form
={{ $json.urls[3] }} // MNDA
={{ $json.urls[4] }} // SOW- Adjust order to your preference (cover first).
Advanced Options → name: ={{ $json.fileName }}
- Response: Returns a
urlto the merged booklet.
Step 8: Download merged file and upload to Google Drive
Node: HTTP Request (GET merged file)
- URL:
={{ $json.url }} - Options → Response → Format:
File - Binary Property:
data
Node: Google Drive → Upload File
Settings:
- Operation: Upload
- Input Data Field Name:
data - Parent Drive from List:
My Drive - Parent Folder by ID: Select your destination folder
What This Does: Uploads the finalized PDF into your chosen Drive folder.
Congrats! You’ve created a streamlined legal packet builder that automatically assembles personalized, shareable, and professional PDF booklets from client emails — no manual intervention needed.
Built something cool with this workflow? Share it with us @pdfdotco and show how you’re automating document magic!
API Reference (links)
- Upload (presigned URL flow): https://docs.pdf.co/api-reference/file-upload/upload
- HTML → PDF (Template): https://docs.pdf.co/api-reference/pdf-from-html/convert-from-template
- Document Parser: https://docs.pdf.co/api-reference/documentparser/overview
- PDF Merge: https://docs.pdf.co/api-reference/merge/pdf
Related Tutorials


