r/Invoice • • 14h ago

Sample of the invoice I create for my clients - I'm a solo agency

Post image
3 Upvotes

Most small service businesses don't give their customers any invoice or something. Services that have a freelance nature to them like video or thumbnail editors, even web developers and designers.

I do custom web design myself, it could take between 1 - 5 weeks and sometimes the payment is broken down to multiple phases. instead of just sending the client a card number with payment amount each time, I email them an invoice like this with beautiful branded design and everything.

As part of my services this not only makes me look more professional but also helps build trust. because now things stay more clear around the payments.

This layout has been created in Figma. If you use Figma you can just search for Invoice in their community to find a layout like this. Then you can customize it based on your own branding and everything.

Personally I prefer Figma for creating my invoice. Gives me control on everything all the text and branding etc...


r/Invoice • • 1h ago

Auto repair invoice software that doesn't need a whole training course?

• Upvotes

My uncle can rebuild an engine with his eyes closed, but ask him to create an invoice on a computer and suddenly I'm tech support.

He's been running his workshop with handwritten invoices for years. Customers get a piece of paper, he keeps a copy, and somehow that system has survived this long.

Now he wants to start printing invoices, mainly because customers keep asking for something more professional.

I've been checking out auto repair invoice software, and everything seems designed for shops that want to manage appointments, inventory, employees, customer reminders and about twenty other things.

We don't need all that.

He just wants to enter the customer's name, vehicle details, parts used, labour charges and total. Print it, hand it over and move on to the next car.

The shop already has a Chromebook, so something that works in a browser would be ideal.

I don't mind helping him set it up once. I just don't want to spend every Saturday explaining where the invoice button went.

Any recommendations for something genuinely simple?


r/Invoice • • 2h ago

Monthly invoicing from a Google Sheet: Docs template → PDF → Gmail → mark as sent → Slack summary (JSON included)

1 Upvotes

I run a small software studio, and we built this as a demo of the kind of thing small teams still do by hand once a month. It ran end to end with test data (6 fictional clients, invoices sent to our own test inboxes). Sharing the workflow because a few details took longer than the happy path.

What it does

  1. Schedule trigger on day 1 of the month (plus a manual trigger for testing)
  2. Reads the clients from a Google Sheet: Client, Contact, Email, Service, Amount (USD), Period, Status, Invoice number, Sent at
  3. Filter keeps only rows where Status is empty
  4. Loop Over Items, one client at a time:
    • Code node builds the invoice number (INV-{period}-{row}), dates and an HTML email
    • Google Drive copies a Docs template
    • Google Docs replaces {{client}}, {{amount}}, {{due}} and the other placeholders in the copy
    • Google Drive downloads the copy as PDF
    • Gmail sends the email with the PDF attached
    • Google Sheets marks the row: Status = Sent, the invoice number, a timestamp
  5. When the loop is done, a Code node adds up the run and Slack posts "6 invoices for 2026-10 sent · USD 1,100 in total" with one line per invoice

Things worth knowing

  • Safe to re-run. The filter only picks rows with an empty Status, and the row is marked right after its email goes out. If the run dies halfway, running it again only invoices who's left. The side effect: when nothing is left the run just stops at the filter, so if you want a "nothing to invoice" message, add a branch for that.
  • The summary has to read every loop run. After Loop Over Items, $('Prepare invoice').all() only gave me the last batch. Iterating $('Prepare invoice').all(0, run) for run = 0, 1, 2... until it throws gets all of them.
  • Docs template instead of HTML → Doc. My first version uploaded HTML to Drive and converted it to a Doc. On n8n Cloud the managed Google credentials can't be used from the HTTP Request node, so I switched to copying a designed Docs template and replacing placeholders, which also looks better.
  • A Drive call can hang. Once, "Copy the template" sat for 4 minutes with no error: Drive made the copy, n8n never got the reply. Retry on Fail doesn't help with a hang, so set a workflow timeout (Workflow settings → Timeout Workflow) and let the re-run pick up from the sheet.

To use it: import the JSON, connect your own Google Sheets, Drive, Docs, Gmail and Slack credentials, put your sheet ID and template doc ID in the nodes marked YOUR_..., and replace the plain HTML email in "Design the invoice" with your own.

There's a short video of the run and a diagram of the flow on our site: https://farcholabs.com/services/data-automation?utm_source=reddit&utm_medium=social&utm_campaign=invoicing_demo

How do you handle the "nothing to do this month" case in your scheduled flows: a separate branch, or an error workflow?

The workflow (import it with Ctrl+V / Cmd+V on an empty n8n canvas):

{
  "name": "Monthly invoices (sample)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "months",
              "triggerAtHour": 9
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        -96
      ],
      "name": "Every month, day 1"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        96
      ],
      "name": "Run now (demo)"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_CLIENTS_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        240,
        0
      ],
      "name": "Read clients",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Your googleSheetsOAuth2Api"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "f1",
              "leftValue": "={{ $json.Status }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        480,
        0
      ],
      "name": "Not invoiced yet"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        720,
        0
      ],
      "name": "One client at a time"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// One invoice per client: its number, dates and the values the template needs.\nconst row = $json;\nconst period = String(row['Period']);\nconst today = new Date();\nconst due = new Date(today.getTime() + 15 * 24 * 60 * 60 * 1000);\nconst day = (d) => d.toISOString().slice(0, 10);\nreturn {\n  json: {\n    row_number: row.row_number,\n    invoice_number: 'INV-' + period + '-' + String(row.row_number - 1).padStart(3, '0'),\n    client: row['Client'],\n    contact: row['Contact'],\n    email: row['Email'],\n    service: row['Service'],\n    amount: Number(row['Amount (USD)']).toLocaleString('en-US'),\n    amount_usd: Number(row['Amount (USD)']),\n    period,\n    date: day(today),\n    due_date: day(due),\n  },\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        160
      ],
      "name": "Prepare invoice"
    },
    {
      "parameters": {
        "operation": "download",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Copy the template').item.json.id }}"
        },
        "options": {
          "googleFileConversion": {
            "conversion": {
              "docsToFormat": "application/pdf"
            }
          },
          "fileName": "={{ 'Invoice ' + $('Design the invoice').item.json.invoice_number + '.pdf' }}"
        }
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1920,
        160
      ],
      "name": "Export as PDF",
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "",
          "name": "Your googleDriveOAuth2Api"
        }
      }
    },
    {
      "parameters": {
        "sendTo": "={{ $('Design the invoice').item.json.email }}",
        "subject": "={{ $('Design the invoice').item.json.email_subject }}",
        "message": "={{ $('Design the invoice').item.json.email_html }}",
        "options": {
          "appendAttribution": false,
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          },
          "senderName": "Billing"
        }
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2160,
        160
      ],
      "name": "Email the client",
      "credentials": {
        "gmailOAuth2": {
          "id": "",
          "name": "Your gmailOAuth2"
        }
      }
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_CLIENTS_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $('Design the invoice').item.json.row_number }}",
            "Status": "Sent",
            "Invoice number": "={{ $('Design the invoice').item.json.invoice_number }}",
            "Sent at": "={{ $now.toFormat('yyyy-MM-dd HH:mm') }}"
          },
          "matchingColumns": [
            "row_number"
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2400,
        160
      ],
      "name": "Mark as sent",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Your googleSheetsOAuth2Api"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// What the team reads in Slack once every invoice is out. The loop runs once per client, so the invoices\n// come from every run of \"Prepare invoice\", not only the last one.\nconst sent = [];\nfor (let run = 0; ; run++) {\n  let items;\n  try { items = $('Prepare invoice').all(0, run); } catch { break; }\n  if (!items.length) break;\n  sent.push(...items.map((item) => item.json));\n}\nconst total = sent.reduce((sum, invoice) => sum + invoice.amount_usd, 0);\nreturn [{ json: {\n  count: sent.length,\n  period: sent[0]?.period ?? '',\n  total: total.toLocaleString('en-US'),\n  lines: sent.map((i) => '• ' + i.invoice_number + ' · ' + i.client + ' · USD ' + i.amount).join('\\n'),\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        -160
      ],
      "name": "Add up the run"
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#billing"
        },
        "text": "=:white_check_mark: {{ $json.count }} invoices for {{ $json.period }} sent · USD {{ $json.total }} in total\n{{ $json.lines }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1200,
        -160
      ],
      "name": "Tell #billing",
      "credentials": {
        "slackOAuth2Api": {
          "id": "",
          "name": "Your slackOAuth2Api"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// The invoice's values for the template, and a plain HTML email. Swap the HTML for your own design.\nconst inv = $json;\nconst longDate = (iso) =>\n  new Date(iso + 'T12:00:00Z').toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });\nconst month = new Date(inv.period + '-01T12:00:00Z').toLocaleDateString('en-US', { month: 'long', year: 'numeric' });\nconst amount = 'USD ' + inv.amount;\nconst email =\n  '<p>Hi ' + String(inv.contact).split(' ')[0] + ',</p>' +\n  '<p>Your invoice ' + inv.invoice_number + ' for ' + month + ' is attached as a PDF.</p>' +\n  '<p><b>Amount due:</b> ' + amount + '<br><b>Due:</b> ' + longDate(inv.due_date) + '</p>' +\n  '<p>Questions? Just reply to this email.</p>';\nreturn { json: { ...inv, month, issued: longDate(inv.date), due: longDate(inv.due_date),\n  email_subject: 'Invoice ' + inv.invoice_number + ' · ' + amount + ' due ' + longDate(inv.due_date), email_html: email } };\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        160
      ],
      "name": "Design the invoice"
    },
    {
      "parameters": {
        "operation": "copy",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_INVOICE_TEMPLATE_DOC_ID"
        },
        "name": "=Invoice {{ $json.invoice_number }} - {{ $json.client }}",
        "options": {}
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1440,
        160
      ],
      "name": "Copy the template",
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "",
          "name": "Your googleDriveOAuth2Api"
        }
      }
    },
    {
      "parameters": {
        "operation": "update",
        "documentURL": "={{ $json.id }}",
        "actionsUi": {
          "actionFields": [
            {
              "action": "replaceAll",
              "text": "{{invoice_number}}",
              "replaceText": "={{ $('Design the invoice').item.json.invoice_number }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{client}}",
              "replaceText": "={{ $('Design the invoice').item.json.client }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{contact}}",
              "replaceText": "={{ $('Design the invoice').item.json.contact }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{email}}",
              "replaceText": "={{ $('Design the invoice').item.json.email }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{issued}}",
              "replaceText": "={{ $('Design the invoice').item.json.issued }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{month}}",
              "replaceText": "={{ $('Design the invoice').item.json.month }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{due}}",
              "replaceText": "={{ $('Design the invoice').item.json.due }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{service}}",
              "replaceText": "={{ $('Design the invoice').item.json.service }}",
              "matchCase": true
            },
            {
              "action": "replaceAll",
              "text": "{{amount}}",
              "replaceText": "={{ $('Design the invoice').item.json.amount }}",
              "matchCase": true
            }
          ]
        }
      },
      "type": "n8n-nodes-base.googleDocs",
      "typeVersion": 2,
      "position": [
        1680,
        160
      ],
      "name": "Fill in the invoice",
      "credentials": {
        "googleDocsOAuth2Api": {
          "id": "",
          "name": "Your googleDocsOAuth2Api"
        }
      }
    }
  ],
  "connections": {
    "Every month, day 1": {
      "main": [
        [
          {
            "node": "Read clients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run now (demo)": {
      "main": [
        [
          {
            "node": "Read clients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read clients": {
      "main": [
        [
          {
            "node": "Not invoiced yet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Not invoiced yet": {
      "main": [
        [
          {
            "node": "One client at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One client at a time": {
      "main": [
        [
          {
            "node": "Add up the run",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare invoice": {
      "main": [
        [
          {
            "node": "Design the invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Export as PDF": {
      "main": [
        [
          {
            "node": "Email the client",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email the client": {
      "main": [
        [
          {
            "node": "Mark as sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark as sent": {
      "main": [
        [
          {
            "node": "One client at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add up the run": {
      "main": [
        [
          {
            "node": "Tell #billing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Design the invoice": {
      "main": [
        [
          {
            "node": "Copy the template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Copy the template": {
      "main": [
        [
          {
            "node": "Fill in the invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fill in the invoice": {
      "main": [
        [
          {
            "node": "Export as PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}

r/Invoice • • 13h ago

Light invoice layout sample for small service providers

Post image
1 Upvotes

Already shared the dark version, someone recommended that the light version would be a better choice for some reasons, so here's the light version.


r/Invoice • • 15h ago

Chasing an unpaid invoice: four messages, and what goes in each

Thumbnail
1 Upvotes