{
  "name": "Generate PDF invoices from webhook data with PDFMint",
  "nodes": [
    {
      "id": "Sticky Note",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -800,
        -380
      ],
      "parameters": {
        "color": 1,
        "width": 620,
        "height": 1080,
        "content": "## Generate PDF invoices from webhook data with PDFMint\n\n**Who's it for**\nAnyone whose shop, billing system or internal tool can POST JSON and needs a finished PDF invoice back in the same request — without running a headless browser, and without a template editor to log into.\n\n**How it works**\n1. **Receive invoice data** accepts a POST with the customer, the line items and the currency.\n2. **Set your company details** is the one node you edit: company name, address, VAT rate, payment terms and footer note.\n3. **Check the request is complete** rejects a request that has no customer or no line items *before* a PDF is rendered, so a broken payload gets a readable 422 instead of an invoice that says \"undefined\".\n4. **Calculate line totals and VAT** does the arithmetic in one place: per-line amounts, subtotal, VAT at your rate, grand total, and the due date derived from your payment terms. The HTML stays a layout, not a calculator.\n5. **Render the invoice as PDF** builds the document with PDFMint. The layout is plain HTML and CSS inside the node, so you change the design by editing markup.\n6. **Did the render succeed?** checks the render result. On success **Return the PDF** answers the original request with the file; if PDFMint could not be reached or refused the job, **Explain what went wrong** and **Return the error as JSON** answer with the right error status — 422 for a broken payload, 500 when the render itself fails — plus the reason. The caller always gets an answer.\n\n**How to set up**\n- Add a PDFMint credential (free plan: 10 documents a month, no card).\n- Open **Set your company details** and put in your own company name, address, VAT rate and payment terms.\n- Activate the workflow and POST to the production webhook URL.\n\n**Requirements**\n- A PDFMint API key from pdf.mintapis.com.\n- ⚠️ This template uses a community node, so it runs on **self-hosted n8n only**.\n\n**How to customize**\nEdit the HTML in **Render the invoice as PDF** for your own branding — line items are looped in one expression, so any number of rows works. Set `vat_percent` to `0` if you do not charge VAT. Swap **Return the PDF** for a Gmail, Drive or S3 node to deliver the invoice instead of returning it."
      }
    },
    {
      "id": "Sticky Note1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -100,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "### 1. The request comes in\nPOST the invoice as JSON. Required: `customer_name` and a `line_items` array of `description` / `quantity` / `unit_price`. Optional: `currency`, `customer_email`, `invoice_number`."
      }
    },
    {
      "id": "Sticky Note2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        240,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "### 2. Edit this node, not the others\nYour company name, address, VAT rate, payment terms and footer note. Every other node reads its settings from here."
      }
    },
    {
      "id": "Sticky Note3",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        580,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "### 3. Refuse a broken payload early\nNo customer or no line items means no PDF is worth rendering. The request is answered with a readable reason instead of an invoice full of `undefined`."
      }
    },
    {
      "id": "Sticky Note4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        920,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "### 4. All the arithmetic, in one place\nPer-line amounts, subtotal, VAT at your rate, grand total, and the due date derived from your payment terms — computed here so the HTML stays a layout."
      }
    },
    {
      "id": "Sticky Note5",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1260,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "### 5. HTML in, PDF out\nThe invoice layout is ordinary HTML and CSS in this node. No template editor, no template ids — edit the markup and the next render changes."
      }
    },
    {
      "id": "Sticky Note6",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1600,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "### 6. Did a PDF actually come back?\nThe render node is set to *continue* on failure, so an unreachable API or a refused job arrives here as data instead of stopping the workflow. This node decides which of the two answers the caller gets."
      }
    },
    {
      "id": "Sticky Note7",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1940,
        -480
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 220,
        "content": "### 7. The caller always gets an answer\nOn success the POST is answered with the PDF itself; on failure with an error status (422 for a bad payload, 500 when the render itself fails) and the reason, so the request never hangs. Replace **Return the PDF** with Gmail, Drive or S3 if you would rather deliver the file somewhere."
      }
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "invoice",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "n1",
      "name": "Receive invoice data",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -40,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f1",
              "name": "seller_name",
              "value": "Your Company Ltd",
              "type": "string"
            },
            {
              "id": "f2",
              "name": "seller_address",
              "value": "1 Example Street, 12345 Your City",
              "type": "string"
            },
            {
              "id": "f3",
              "name": "footer_note",
              "value": "Thank you for your business.",
              "type": "string"
            },
            {
              "id": "f4",
              "name": "default_currency",
              "value": "EUR",
              "type": "string"
            },
            {
              "id": "f5",
              "name": "vat_percent",
              "value": 19,
              "type": "number"
            },
            {
              "id": "f6",
              "name": "payment_terms_days",
              "value": 14,
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "n2",
      "name": "Set your company details",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        300,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.body.customer_name }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            },
            {
              "id": "c2",
              "leftValue": "={{ $json.body.line_items }}",
              "rightValue": "",
              "operator": {
                "type": "array",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "n3",
      "name": "Check the request is complete",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        640,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Everything the invoice needs is worked out here, once, so the HTML stays a layout\n// and not a calculator: per-line amounts, subtotal, VAT, grand total, and the dates\n// that follow from your payment terms.\nconst item = $input.first().json;\nconst body = item.body || {};\n\nconst currency = String(body.currency || item.default_currency || 'EUR').toUpperCase();\nconst vatPercent = Number(item.vat_percent) || 0;\nconst termsDays = Number(item.payment_terms_days) || 14;\nconst money = (n) => currency + ' ' + n.toFixed(2);\n\nconst rows = (body.line_items || []).map((li) => {\n  const quantity = Number(li.quantity) || 0;\n  const unitPrice = Number(li.unit_price) || 0;\n  const amount = Math.round(quantity * unitPrice * 100) / 100;\n  return {\n    description: String(li.description || 'Item'),\n    quantity,\n    unit_display: money(unitPrice),\n    amount,\n    amount_display: money(amount),\n  };\n});\n\nconst subtotal = Math.round(rows.reduce((sum, r) => sum + r.amount, 0) * 100) / 100;\nconst vat = Math.round(subtotal * vatPercent) / 100;\nconst total = Math.round((subtotal + vat) * 100) / 100;\n\nconst pad = (n) => String(n).padStart(2, '0');\nconst issued = new Date();\nconst due = new Date(issued.getTime() + termsDays * 86400000);\nconst stamp = `${issued.getFullYear()}${pad(issued.getMonth() + 1)}${pad(issued.getDate())}`;\nconst show = (d) => d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });\n\nreturn [{\n  json: {\n    seller_name: item.seller_name,\n    seller_address: item.seller_address,\n    footer_note: item.footer_note,\n    customer_name: body.customer_name,\n    customer_email: body.customer_email || '',\n    invoice_number: body.invoice_number || `INV-${stamp}-1`,\n    issue_date: show(issued),\n    due_date: show(due),\n    currency,\n    vat_percent: vatPercent,\n    rows,\n    subtotal,\n    subtotal_display: money(subtotal),\n    vat,\n    vat_display: money(vat),\n    total,\n    total_display: money(total),\n  },\n}];"
      },
      "id": "n4",
      "name": "Calculate line totals and VAT",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        980,
        -80
      ]
    },
    {
      "parameters": {
        "resource": "document",
        "operation": "pdf",
        "source": "html",
        "html": "=<!doctype html><html><head><meta charset=\"utf-8\"><style>\nbody{font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#111;margin:48px}\nh1{font-size:26px;margin:0 0 4px} .muted{color:#666;font-size:13px}\n.row{display:flex;justify-content:space-between;margin-top:32px}\ntable{width:100%;border-collapse:collapse;margin-top:28px;font-size:14px}\nth{text-align:left;border-bottom:2px solid #111;padding:8px 0;font-size:12px;text-transform:uppercase;letter-spacing:.04em}\ntd{padding:10px 0;border-bottom:1px solid #e5e5e5}\ntr{break-inside:avoid}\n.right{text-align:right}\n.totals{margin-top:24px;margin-left:auto;width:280px;font-size:14px}\n.totals div{display:flex;justify-content:space-between;padding:6px 0}\n.totals .grand{font-size:18px;font-weight:700;border-top:2px solid #111;padding-top:12px}\n</style></head><body>\n<h1>{{ $json.seller_name }}</h1>\n<div class=\"muted\">{{ $json.seller_address }}</div>\n<div class=\"row\">\n  <div><strong>Billed to</strong><br>{{ $json.customer_name }}<br><span class=\"muted\">{{ $json.customer_email }}</span></div>\n  <div class=\"right\"><strong>Invoice {{ $json.invoice_number }}</strong><br><span class=\"muted\">Issued {{ $json.issue_date }}<br>Due {{ $json.due_date }}</span></div>\n</div>\n<table><thead><tr><th>Description</th><th class=\"right\">Qty</th><th class=\"right\">Unit</th><th class=\"right\">Amount</th></tr></thead><tbody>\n{{ $json.rows.map(r => '<tr><td>' + r.description + '</td><td class=\"right\">' + r.quantity + '</td><td class=\"right\">' + r.unit_display + '</td><td class=\"right\">' + r.amount_display + '</td></tr>').join('') }}\n</tbody></table>\n<div class=\"totals\">\n  <div><span>Subtotal</span><span>{{ $json.subtotal_display }}</span></div>\n  <div><span>VAT {{ $json.vat_percent }}%</span><span>{{ $json.vat_display }}</span></div>\n  <div class=\"grand\"><span>Total</span><span>{{ $json.total_display }}</span></div>\n</div>\n<p class=\"muted\" style=\"margin-top:40px\">{{ $json.footer_note }}</p>\n</body></html>",
        "options": {},
        "fileName": "=invoice-{{ $json.invoice_number }}.pdf"
      },
      "id": "n5",
      "name": "Render the invoice as PDF",
      "type": "n8n-nodes-pdfmint.pdfMint",
      "typeVersion": 1,
      "position": [
        1320,
        -80
      ],
      "onError": "continueRegularOutput",
      "credentials": {
        "pdfMintApi": {
          "name": "PDFMint account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c3",
              "leftValue": "={{ $json.error }}",
              "rightValue": "",
              "operator": {
                "type": "object",
                "operation": "notExists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "n6",
      "name": "Did the render succeed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1660,
        -80
      ]
    },
    {
      "parameters": {
        "respondWith": "binary",
        "options": {}
      },
      "id": "n7",
      "name": "Return the PDF",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2000,
        -180
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "error",
              "value": "invoice_not_generated",
              "type": "string"
            },
            {
              "id": "e2",
              "name": "message",
              "value": "={{ $json.error ? 'The invoice could not be rendered: ' + ($json.error.message || $json.error) : 'Send a customer_name and a non-empty line_items array.' }}",
              "type": "string"
            },
            {
              "id": "e3",
              "name": "status_code",
              "value": "={{ $json.error ? 500 : 422 }}",
              "type": "number"
            }
          ]
        },
        "includeOtherFields": false,
        "options": {}
      },
      "id": "n8",
      "name": "Explain what went wrong",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1660,
        180
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {
          "responseCode": "={{ $json.status_code }}"
        }
      },
      "id": "n9",
      "name": "Return the error as JSON",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2000,
        180
      ]
    }
  ],
  "connections": {
    "Receive invoice data": {
      "main": [
        [
          {
            "node": "Set your company details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set your company details": {
      "main": [
        [
          {
            "node": "Check the request is complete",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the request is complete": {
      "main": [
        [
          {
            "node": "Calculate line totals and VAT",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain what went wrong",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate line totals and VAT": {
      "main": [
        [
          {
            "node": "Render the invoice as PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Render the invoice as PDF": {
      "main": [
        [
          {
            "node": "Did the render succeed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Did the render succeed?": {
      "main": [
        [
          {
            "node": "Return the PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain what went wrong",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Explain what went wrong": {
      "main": [
        [
          {
            "node": "Return the error as JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}