{
  "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('&', '&amp;')\n    .replaceAll('<', '&lt;')\n    .replaceAll('>', '&gt;')\n    .replaceAll('\"', '&quot;')\n    .replaceAll(\"'\", '&#39;');\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
          }
        ]
      ]
    }
  }
}