This workflow corresponds to n8n.io template #12327 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Generate and email invoices, quotes, and receipts from webhook JSON",
"nodes": [
{
"id": "gg000007-0000-0000-0000-000000000001",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-560,
-240
],
"parameters": {
"color": 3,
"width": 460,
"height": 1340,
"content": "## Generate and email invoices, quotes, and receipts from webhook JSON\n\n### Who is it for\nAnyone with a system that already knows about orders \u2014 an online store, CRM, booking tool, or internal app \u2014 who wants professional billing documents sent to customers without building PDF infrastructure.\n\n### How it works\n1. Your system POSTs order JSON to the webhook (documentType picks invoice, quote, estimate, receipt, or proforma)\n2. The workflow builds a styled document with line items, tax, and totals\n3. The Acrewity community node renders it to PDF\n4. Gmail delivers the PDF to the customer within seconds\n5. Your system receives a JSON confirmation with the document number\n6. Bad payloads (missing email, broken items) get a clear HTTP 400 with the reason instead of a silent failure\n\n### How to set up\n1. Install the verified community node @acrewity/n8n-nodes-acrewity and add your Acrewity API credential (free key at acrewity.com, 100 free credits/month)\n2. Connect your Gmail credential to the email node\n3. Edit the Workflow configuration node: company name, logo, default tax rate, currency\n4. Activate and POST to the webhook URL\n\n### Sample payload\n{\n \"documentType\": \"invoice\",\n \"documentNumber\": \"INV-2026-001\",\n \"customerName\": \"John Smith\",\n \"customerEmail\": \"john@example.com\",\n \"dueDate\": \"2026-08-15\",\n \"taxRate\": 0.13,\n \"items\": [{\"name\": \"Widget\", \"quantity\": 2, \"price\": 9.99}]\n}\n\n### How to customize\nDocument layout lives in Build document HTML (marked CHANGE ME), email copy in the Gmail node. Add a Drive or Slack node after Gmail to archive or notify."
},
"typeVersion": 1
},
{
"id": "gg000007-0000-0000-0000-000000000002",
"name": "Receive order data",
"type": "n8n-nodes-base.webhook",
"position": [
0,
0
],
"parameters": {
"path": "generate-document",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "gg000007-0000-0000-0000-000000000003",
"name": "Workflow configuration",
"type": "n8n-nodes-base.set",
"position": [
220,
0
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-1",
"name": "companyName",
"type": "string",
"value": "Your Company Inc."
},
{
"id": "cfg-2",
"name": "companyLogoUrl",
"type": "string",
"value": ""
},
{
"id": "cfg-3",
"name": "defaultTaxRate",
"type": "number",
"value": 0
},
{
"id": "cfg-4",
"name": "currencySymbol",
"type": "string",
"value": "$"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "gg000007-0000-0000-0000-000000000004",
"name": "Build document HTML",
"type": "n8n-nodes-base.code",
"position": [
440,
0
],
"parameters": {
"jsCode": "// ================================================================\n// BUILD THE DOCUMENT HTML\n//\n// This node takes the JSON your system POSTs to the webhook and\n// turns it into the HTML that becomes the PDF. It supports five\n// document types: invoice, quote, estimate, receipt, proforma.\n//\n// It is safe to edit this code. The places you are most likely\n// to change are marked with: CHANGE ME\n// ================================================================\n\n\n// ---- STEP 1: Get the incoming data -----------------------------\n// The webhook wraps your POSTed JSON in \"body\". \"data\" also holds\n// the settings from the \"Workflow configuration\" node.\nconst settings = $input.first().json; // workflow configuration\nconst order = $input.first().json.body || {}; // your POSTed JSON\n\n\n// ---- STEP 2: Helper that keeps the HTML safe --------------------\n// Converts characters like < and & so customer-entered text cannot\n// break the document layout. Wrap ANY text you print in esc(...).\nfunction esc(text) {\n return String(text ?? '')\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''');\n}\n\n\n// ---- STEP 3: Document type settings ------------------------------\n// Each type gets its own title, number prefix, and optional fields.\n// Add your own types here if you need more. CHANGE ME\nconst typeSettings = {\n invoice: { title: 'INVOICE', prefix: 'INV-', customerLabel: 'Bill To', showDueDate: true, showValidUntil: false, showPaymentMethod: false },\n quote: { title: 'QUOTE', prefix: 'QTE-', customerLabel: 'Quote For', showDueDate: false, showValidUntil: true, showPaymentMethod: false },\n estimate: { title: 'ESTIMATE', prefix: 'EST-', customerLabel: 'Prepared For', showDueDate: false, showValidUntil: true, showPaymentMethod: false },\n receipt: { title: 'RECEIPT', prefix: 'REC-', customerLabel: 'Received From', showDueDate: false, showValidUntil: false, showPaymentMethod: true },\n proforma: { title: 'PROFORMA INVOICE', prefix: 'PRO-', customerLabel: 'Bill To', showDueDate: true, showValidUntil: false, showPaymentMethod: false }\n};\n\nconst docType = String(order.documentType || 'invoice').toLowerCase();\nconst doc = typeSettings[docType] || typeSettings.invoice;\n\n\n// ---- STEP 4: Read the payload fields -----------------------------\n// These names must match the JSON your system sends. CHANGE ME\nconst documentNumber = order.documentNumber || doc.prefix + Date.now();\nconst customerName = order.customerName || 'Customer';\nconst customerEmail = order.customerEmail || '';\nconst customerAddress = order.customerAddress || '';\nconst dueDate = order.dueDate || '';\nconst validUntil = order.validUntil || '';\nconst paymentMethod = order.paymentMethod || '';\nconst notes = order.notes || '';\n\n// Settings from the configuration node\nconst companyName = settings.companyName || 'Your Company Inc.';\nconst logoUrl = settings.companyLogoUrl || '';\nconst currency = settings.currencySymbol || '$';\n// The payload can override the default tax rate per document\nconst taxRate = order.taxRate !== undefined ? Number(order.taxRate) : (Number(settings.defaultTaxRate) || 0);\n\n// ---- Validation -------------------------------------------------\n// A bad payload must not end as a silent failure: instead of stopping,\n// mark the request invalid. The If node after this one routes invalid\n// requests to a response that tells the caller exactly what to fix.\nfunction invalidRequest(reason) {\n return [{ json: { valid: false, errorMessage: reason } }];\n}\n\n// The customer email is required \u2014 the document is delivered by email\nif (!customerEmail || !String(customerEmail).includes('@')) {\n return invalidRequest('customerEmail is required \u2014 the ' + docType + ' is emailed to the customer');\n}\n\n\n// ---- STEP 5: Read the line items ---------------------------------\n// \"items\" must be an array like:\n// [{\"name\":\"Widget\",\"quantity\":2,\"price\":9.99}]\nconst items = Array.isArray(order.items) ? order.items : [];\nif (items.length === 0) {\n return invalidRequest('the payload needs an items array like [{\"name\":\"Widget\",\"quantity\":2,\"price\":9.99}]');\n}\n\n// Every item needs a name, a quantity, and a price\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n if (!item || typeof item.name !== 'string' || item.name.trim() === '') {\n return invalidRequest('item ' + (i + 1) + ' has no \"name\" field');\n }\n if (isNaN(Number(item.quantity)) || Number(item.quantity) <= 0) {\n return invalidRequest('item ' + (i + 1) + ' needs a numeric quantity above zero');\n }\n if (isNaN(Number(item.price))) {\n return invalidRequest('item ' + (i + 1) + ' needs a numeric price');\n }\n}\n\nlet itemsHtml = '';\nlet subtotal = 0;\nfor (const item of items) {\n const quantity = Number(item.quantity) || 0;\n const price = Number(item.price) || 0;\n const lineTotal = quantity * price;\n subtotal = subtotal + lineTotal;\n itemsHtml = itemsHtml +\n '<tr><td>' + esc(item.name) + '</td><td>' + quantity + '</td><td>' +\n currency + price.toFixed(2) + '</td><td>' + currency + lineTotal.toFixed(2) + '</td></tr>';\n}\n\nconst tax = subtotal * taxRate;\nconst total = subtotal + tax;\n\n\n// ---- STEP 6: Optional sections (shown only when relevant) --------\nlet optionalHtml = '';\nif (doc.showDueDate && dueDate) optionalHtml += '<p><strong>Due Date:</strong> ' + esc(dueDate) + '</p>';\nif (doc.showValidUntil && validUntil) optionalHtml += '<p><strong>Valid Until:</strong> ' + esc(validUntil) + '</p>';\nif (doc.showPaymentMethod && paymentMethod) optionalHtml += '<p><strong>Payment Method:</strong> ' + esc(paymentMethod) + '</p>';\n\nconst logoHtml = logoUrl ? '<img src=\"' + esc(logoUrl) + '\" style=\"max-height:60px;max-width:200px;\" alt=\"Logo\">' : '';\nconst addressHtml = customerAddress ? '<p><strong>Address:</strong> ' + esc(customerAddress) + '</p>' : '';\nconst notesHtml = notes ? '<div class=\"notes\"><strong>Notes:</strong><br>' + esc(notes) + '</div>' : '';\nconst taxHtml = taxRate > 0\n ? '<p class=\"right\">Tax (' + (taxRate * 100).toFixed(0) + '%): ' + currency + tax.toFixed(2) + '</p>'\n : '';\n\n\n// ---- STEP 7: The document itself ----------------------------------\n// Normal HTML and CSS \u2014 change colors, fonts, and layout\n// in the <style> block below. CHANGE ME\nconst html = `<!DOCTYPE html>\n<html>\n<head>\n<style>\n body { font-family: Arial, sans-serif; padding: 40px; color: #1e293b; }\n .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }\n h1 { margin: 0 0 4px 0; }\n h2 { color: #64748b; margin: 0; font-weight: 600; }\n table { width: 100%; border-collapse: collapse; margin: 20px 0; }\n th, td { padding: 10px; text-align: left; border-bottom: 1px solid #e2e8f0; }\n th { background: #f1f5f9; }\n .right { text-align: right; }\n .bold { font-weight: bold; font-size: 18px; }\n .notes { margin-top: 30px; padding: 15px; background: #f8fafc; border-left: 3px solid #cbd5e1; }\n</style>\n</head>\n<body>\n\n <div class=\"header\">\n <div>\n <h1>${esc(companyName)}</h1>\n <h2>${doc.title}</h2>\n </div>\n ${logoHtml}\n </div>\n\n <p><strong>${doc.title.charAt(0) + doc.title.slice(1).toLowerCase()} #:</strong> ${esc(documentNumber)}</p>\n <p><strong>Date:</strong> ${new Date().toISOString().slice(0, 10)}</p>\n ${optionalHtml}\n <p><strong>${doc.customerLabel}:</strong> ${esc(customerName)}</p>\n ${addressHtml}\n <p><strong>Email:</strong> ${esc(customerEmail)}</p>\n\n <table>\n <tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>\n ${itemsHtml}\n </table>\n\n <p class=\"right\">Subtotal: ${currency}${subtotal.toFixed(2)}</p>\n ${taxHtml}\n <p class=\"right bold\">Total: ${currency}${total.toFixed(2)}</p>\n\n ${notesHtml}\n\n</body>\n</html>`;\n\n\n// ---- STEP 8: Hand everything to the next nodes --------------------\nreturn [{\n json: {\n valid: true,\n html: html,\n documentType: docType,\n documentTitle: doc.title,\n documentNumber: documentNumber,\n customerName: customerName,\n customerEmail: customerEmail,\n documentFilename: documentNumber + '.pdf',\n emailSubject: 'Your ' + docType + ' ' + documentNumber + ' from ' + companyName\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "gg000007-0000-0000-0000-000000000005",
"name": "Convert document to PDF",
"type": "@acrewity/n8n-nodes-acrewity.acrewity",
"position": [
880,
0
],
"parameters": {
"html": "={{ $json.html }}",
"resource": "html_to_pdf"
},
"retryOnFail": true,
"typeVersion": 1
},
{
"id": "gg000007-0000-0000-0000-000000000006",
"name": "Convert PDF to file",
"type": "n8n-nodes-base.convertToFile",
"position": [
1100,
0
],
"parameters": {
"options": {
"fileName": "={{ $('Build document HTML').item.json.documentFilename }}",
"mimeType": "application/pdf"
},
"operation": "toBinary",
"sourceProperty": "result.data.content",
"binaryPropertyName": "data"
},
"typeVersion": 1.1
},
{
"id": "gg000007-0000-0000-0000-000000000007",
"name": "Email document to customer",
"type": "n8n-nodes-base.gmail",
"position": [
1320,
0
],
"parameters": {
"sendTo": "={{ $('Build document HTML').item.json.customerEmail }}",
"message": "=Hi {{ $('Build document HTML').item.json.customerName }},\n\nYour {{ $('Build document HTML').item.json.documentType }} {{ $('Build document HTML').item.json.documentNumber }} is attached as a PDF.\n\nIf you have any questions, just reply to this email.\n\nBest regards",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{
"property": "data"
}
]
}
},
"subject": "={{ $('Build document HTML').item.json.emailSubject }}"
},
"typeVersion": 2.2
},
{
"id": "gg000007-0000-0000-0000-000000000008",
"name": "Confirm delivery",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1540,
0
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ { success: true, documentType: $('Build document HTML').item.json.documentType, documentNumber: $('Build document HTML').item.json.documentNumber, emailedTo: $('Build document HTML').item.json.customerEmail } }}"
},
"typeVersion": 1.1
},
{
"id": "gg000007-0000-0000-0000-000000000010",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-40,
-270
],
"parameters": {
"width": 420,
"height": 240,
"content": "**1. Intake and configuration**\nYour store, CRM, or app POSTs order JSON here. Company details and defaults live in the Workflow configuration node."
},
"typeVersion": 1
},
{
"id": "gg000007-0000-0000-0000-000000000011",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
420,
-270
],
"parameters": {
"width": 420,
"height": 240,
"content": "**2. Build and convert**\nOne code node handles all five document types (invoice, quote, estimate, receipt, proforma). The Acrewity node renders the PDF."
},
"typeVersion": 1
},
{
"id": "gg000007-0000-0000-0000-000000000012",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1080,
-270
],
"parameters": {
"width": 440,
"height": 240,
"content": "**3. Deliver and confirm**\nGmail sends the PDF to the customer; the caller gets a JSON confirmation with the document number instead of a raw file."
},
"typeVersion": 1
},
{
"id": "gg000007-0000-0000-0000-000000000015",
"name": "Is the request valid?",
"type": "n8n-nodes-base.if",
"position": [
620,
0
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "v1",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.valid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "gg000007-0000-0000-0000-000000000016",
"name": "Reject bad request",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
880,
220
],
"parameters": {
"options": {
"responseCode": 400
},
"respondWith": "json",
"responseBody": "={{ { success: false, error: $json.errorMessage } }}"
},
"typeVersion": 1.1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Receive order data": {
"main": [
[
{
"node": "Workflow configuration",
"type": "main",
"index": 0
}
]
]
},
"Build document HTML": {
"main": [
[
{
"node": "Is the request valid?",
"type": "main",
"index": 0
}
]
]
},
"Convert PDF to file": {
"main": [
[
{
"node": "Email document to customer",
"type": "main",
"index": 0
}
]
]
},
"Is the request valid?": {
"main": [
[
{
"node": "Convert document to PDF",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject bad request",
"type": "main",
"index": 0
}
]
]
},
"Workflow configuration": {
"main": [
[
{
"node": "Build document HTML",
"type": "main",
"index": 0
}
]
]
},
"Convert document to PDF": {
"main": [
[
{
"node": "Convert PDF to file",
"type": "main",
"index": 0
}
]
]
},
"Email document to customer": {
"main": [
[
{
"node": "Confirm delivery",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Businesses and freelancers who need to generate professional PDF documents (invoices, quotes, receipts, estimates) from order/sales data. invoice - Standard invoice with due date quote - Quote/proposal with validity period estimate - Cost estimate with validity period receipt -…
Source: https://n8n.io/workflows/12327/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow receives new signup events, enriches each contact with Twain (and optionally a LinkedIn lookup), drafts a personalized welcome email in Gmail, and posts review-ready previews to Slack, w
Receive orders via webhook with automatic payment verification, stock checking, inventory reservation, shipping label generation, confirmation emails, and Slack notifications.
OTP Verification System. Uses googleSheets, gmail. Webhook trigger; 23 nodes.
A comprehensive n8n workflow template for automating e-commerce return processes with fraud prevention and professional document generation. E-commerce businesses preventing fraudulent returns Custome
Transform your visitor management process with this fully automated, enterprise-grade workflow. The Verified Visitor Pass Generator eliminates manual data entry, prevents fake registrations through em