Affiliate disclosure: some links in this article may be affiliate links. If you sign up through them, we may earn a commission at no extra cost to you. See our Terms of Service.
A customer pays you through Stripe. Stripe records the payment and, if you have it switched on, emails a receipt. But a receipt is not always an invoice. Many customers, especially businesses, want a proper invoice document with your company name, an invoice number, the date and the amount, to file for their own accounting. So at the end of the month you export your payments, open a template, and make invoices by hand, one at a time. Or you pay for an invoicing tool whose main job, in your case, is producing a PDF.
This free n8n workflow does that job on its own. It listens for successful Stripe payments. For each one, it looks up the customer, builds the invoice data, has a PDF made by a community node called PDFops, saves the PDF in an Amazon S3 bucket (cloud file storage), emails it to the customer through Gmail, and adds a line to a Google Sheets ledger. If something goes wrong with the invoice itself, it posts a message to Slack. One detail makes this template more careful than most: it knows that some currencies, such as the Japanese yen, have no cents, and that Stripe reports their amounts differently. In this guide we explain every step in plain English, show with worked examples how amounts and tax are handled, and are direct about the places where this workflow can be wrong, or fail without telling you.
Read this first: it issues invoices and emails customers on its own
- It can invoice twice. It reacts to every successful payment in the Stripe account, including payments that Stripe’s own invoicing or another tool already invoices. Webhooks can also deliver the same event more than once, and there is no duplicate check.
- Some failures are silent. Only a failure while building the invoice data or the PDF, and a missing customer email, post to Slack. If the upload to S3, the email or the Google Sheets row fails, nothing is posted, and the ledger row is added anyway.
- The invoice is simple, and the amounts need checking. It has one line, no address, no tax number and a number that is not sequential. A currency list built into the code gets at least one currency wrong (UGX), and with a tax rate the PDF total can differ from what Stripe charged.
Use Stripe test mode and fake payments first, and check with an accountant or the rules that apply to you whether these PDFs are acceptable as invoices.
Template source: this workflow comes from the n8n Library at n8n.io, the public collection of workflow templates. We did not create it. LeenLabs prepared its JSON for publication and documented it. What we changed is listed in the section What we changed in the original template.
At a glance
- What it does: turns each successful Stripe payment into an invoice PDF, stores it in S3, emails it to the customer, and logs it in Google Sheets.
- Apps used: n8n, Stripe, a PDF community node (PDFops), Amazon S3, Gmail, Google Sheets and Slack.
- Trigger: a Stripe payment event.
- Setup effort: high. Six accounts, a community node, a bucket, a sheet and an internet-reachable n8n.
- Cost to run: no AI service is involved. The PDF service, AWS, Google, Slack and n8n each have their own plans and limits.
- Watch out for: double invoices, silent failures, a currency list with at least one error, tax rounding, invoices that may not meet legal requirements, and customer data going to several services.
- Test status: the code that builds the invoice was tested on simulated payments outside n8n; the full file and the PDF node were not run.
In this guide
- From payment to invoice, by hand
- Words you will see in this guide
- Apps and accounts this workflow talks to
- What to have ready before you import anything
- How amounts and tax are worked out: worked examples
- The big picture: from payment to archive
- Follow the data: what each of the 12 steps does
- Get the workflow JSON
- Install it in fourteen steps
- What it will not do: limits, costs and trade-offs
- Access, privacy and responsible use
- Problems you may meet and how to fix them
- Too much of a puzzle? Let us build or adapt it for you
- What we changed in the original template
- What was verified, and what was not
- Five ways to adapt the workflow
- Frequently asked questions
- Tools used in this workflow
- Suggested reading
- Keep exploring
From payment to invoice, by hand
Payment processors and invoices solve different problems. A processor moves money and keeps records for itself. An invoice is a document addressed to a customer, with the seller’s details, an identifying number, a date and amounts. Whether a customer needs one, and what it must contain, depends on who they are and where you both are. Many small sellers make invoices manually because it is a small, regular, boring task.
This workflow automates the mechanical part. In one run, it does these jobs:
- Catches a successful Stripe payment.
- Ignores payments below the minimum amount, such as zero-amount events.
- Looks up the customer’s name and email in Stripe.
- Converts the amount to a normal figure, with a special rule for currencies with no decimals, and, if you set a tax rate, works the net amount out from the total.
- Creates the invoice PDF through the PDFops community node.
- Stores the PDF in an S3 bucket and logs a row in Google Sheets.
- Emails the PDF to the customer, or tells Slack there is no address.
- Alerts Slack when the invoice data or the PDF fails.
It suits a small seller that takes one-off payments through Stripe, does not use Stripe’s own invoicing for them, and needs a simple archive-quality PDF. It does not suit you if you sell subscriptions (Stripe Billing already creates invoices), if you need several line items, addresses, tax numbers or sequential numbering, or if you handle refunds and credit notes.
Words you will see in this guide
- n8n (say “n-eight-n”): a tool that connects apps and makes them follow your instructions automatically.
- Workflow: one set of instructions in n8n, drawn as boxes connected by lines.
- Node: one box in a workflow. Each node does one job.
- Trigger: the node that starts a workflow. Here, a Stripe event.
- Webhook: a message that one service sends to another’s web address when something happens. Stripe uses one to tell n8n about a payment.
- PaymentIntent: Stripe’s record of one attempt to collect a payment. The event
payment_intent.succeededsays it worked. - Smallest currency unit: how Stripe writes amounts: in cents for dollars, so 1999 means 19.99.
- Zero-decimal currency: a currency with no cents, such as the Japanese yen. Stripe writes 500 yen as 500.
- Net, tax and total: the price before tax, the tax, and the sum. A net amount derived backwards is worked out from the total.
- Community node: an extra node written by someone outside n8n, installed separately. PDFops is one.
- S3 (Amazon Simple Storage Service): cloud file storage, organized in “buckets”.
- Ledger: a running list of transactions. Here, a Google Sheets tab.
- Credential: a saved login that lets n8n use one of your accounts. The JSON file does not contain yours.
- Test mode: a Stripe setting where payments are fake. Use it first.
Apps and accounts this workflow talks to
- n8n: runs the workflow. It must be reachable from the internet over a secure (HTTPS) address, because Stripe has to call it.
- Stripe: the source of the payments. The workflow registers its listener in the Stripe account of the credential you connect.
- PDFops: a community node and an outside service that renders the PDF. The invoice data, including the customer’s name or email and the amount, goes to that service. We could not verify the provider’s home page, so we do not link to it; you can find the node by searching “PDFops” in n8n. Read its terms and limits before use.
- Amazon S3: stores the PDF.
- Gmail: sends the invoice to the customer from the account you connect.
- Google Sheets: holds the ledger.
- Slack: receives the problem messages.
Costs: the workflow file is free. n8n, the PDF service, AWS, Google and Slack each have their own plans and limits. The original notes mention a free tier for the PDF service; we could not verify its limits, so check the provider’s current terms. We do not quote prices because they change.
What to have ready before you import anything
- An n8n installation that can use community nodes, and the PDFops community node installed (
n8n-nodes-pdfops). Without it, the file imports with a missing node. - A Stripe account, and its test mode keys for the first tests.
- An AWS account with an S3 bucket, private, and an access key that may write to it.
- A Google Sheets spreadsheet with a tab named exactly
Invoices, with these headers in row 1:Date,Total,Invoice,Currency,Customer,Payment ID. - The address (URL) of the spreadsheet.
- A Gmail account to send from, ideally a business address.
- A Slack workspace with a channel for billing messages (the file uses
#billing), and the Slack app added to that channel. - Your company name, an invoice prefix (the file uses
INV), and your tax rate if you want one shown. - A decision: should this workflow invoice every payment in the Stripe account? If not, do not turn it on yet.
How amounts and tax are worked out: worked examples
The Code node called Build Invoice Payload converts the amount. It divides by 100, except for currencies in a fixed list of zero-decimal currencies, which it leaves as they are. We ran it on simulated payments:
| Simulated payment | Invoice shows | Right? |
|---|---|---|
| USD, Stripe amount 1999 | 19.99 USD | Yes. |
| JPY, Stripe amount 5000 | 5000 JPY | Yes. Yen has no cents. |
| KRW, Stripe amount 12000 | 12000 KRW | Yes. |
| ISK, Stripe amount 500 | 5 ISK | Yes. Stripe documents ISK as a two-decimal value that is always whole. |
| UGX, Stripe amount 500 | 500 UGX | No. Stripe’s currencies page says to charge 5 UGX you provide 500. The list in the code treats UGX as a currency with no decimals, so this invoice is 100 times too large. |
| USD 120.00 with a tax rate of 20 | Net 100.00, total 120.00 | Yes: 100 plus 20 percent tax is 120. |
| USD 19.99 with a tax rate of 7.5 | Net 18.60 | The PDF service then adds 7.5 percent tax to 18.60, which, under the usual rounding, gives a total of 20.00 instead of 19.99. Under an assumed rule, see below. |
About the tax rounding. The workflow gives the PDF service a net price and a rate, and the service works out the tax and the total itself. When the amount cannot be split exactly into a net with two decimals plus rate, the result is a cent off. The original code comment says this affects about 10 percent of amount and rate pairs. We could not see how the PDF service rounds, so we simulated it with a common rule (tax rounded to the cent, then added to the net). Under that assumption, the total differed from the charge for about 5 percent of amounts at a 5 percent rate, 7 percent at 7.5, 9 percent at 10, 16 percent at 19 and 17 percent at 20 and 21. Treat these as estimates for our assumed rule, not measurements of the real service. The safe setting is the default: taxRate at 0, which makes the invoice total equal to the charge exactly.
For a currency you accept that is not on your radar, check it against Stripe’s supported currencies page. Anything that is neither a standard two-decimal currency nor in the code’s list may be converted wrongly, and we could not confirm from that page how Stripe represents every currency that has three decimals in real life.
Two more details visible in the simulation. The invoice number is the prefix plus the last 8 characters of the payment’s identifier, in capital letters, such as INV-F12345GH. Two identifiers that differ only in capital letters in those 8 characters produced the same number in our test, which would overwrite the earlier PDF in S3. This is very unlikely but not impossible. And the invoice date is the date of the payment in universal time, which can be one day off from the customer’s local date.
The big picture: from payment to archive
- Catch. Stripe tells n8n a payment succeeded.
- Filter. Payments below the minimum stop.
- Look up. The customer is fetched from Stripe.
- Build. The invoice data is prepared.
- Render. The PDF service makes the PDF.
- Deliver. The PDF is stored in S3, a row is added to the ledger, and the PDF is emailed if an address is known, or Slack is told.
- Alert. If building or rendering fails, Slack is told.
Follow the data: what each of the 12 steps does
The names below match the names in the JSON file exactly.
Payment Succeeded
The trigger: a Stripe Trigger set to the event payment_intent.succeeded. When you turn the workflow on, n8n registers its listener in your Stripe account. The event carries a package of data about the payment, and the PaymentIntent itself sits inside it. It fires for every successful payment in that Stripe account and mode, whatever created it.
Set Invoice Defaults
Holds your settings in one place: companyName (placeholder YOUR_COMPANY_NAME), invoicePrefix (INV), taxRate (0), defaultLineItem (“Payment received”), invoiceNotes (“Thank you for your business.”), s3Bucket (placeholder YOUR_S3_BUCKET_NAME) and minimumAmount (1). The minimum is in the smallest currency unit, so 500 means 5.00 in dollars but 500 in yen.
Has Billable Amount?
A yes-or-no node. It takes the amount received (or the amount if that is missing) and asks whether it is at least the minimum. A yes goes on to the customer lookup. A no goes to the next node. In our check, an amount of 0 stopped, and an amount of 1 passed.
Skip Zero-Amount Payment
A node that does nothing. It is only the place where payments below the minimum end, with no record and no message.
Fetch Stripe Customer
A Stripe node that fetches the customer named in the payment. It is set to continue even if it fails. That is useful for guest checkouts, which have no customer, but it also means that any other failure, such as a wrong key, is hidden: the workflow simply carries on without a name.
Build Invoice Payload
The Code node described above. It converts the amount; works out the net if a tax rate is set; picks the “bill to” text (the customer’s name, else the customer’s email, else the receipt email on the payment, else the word “Customer”); and prepares the invoice: from, to, invoice number, date, currency, tax rate, notes and one line with the payment’s description (or the default text), quantity 1 and the price. It also keeps the customer’s email (customer, then receipt email, else empty), the payment identifier, the charged total and the bucket name. If it fails, an error path leads to Slack.
PDFops Generate Invoice
The community node that sends the invoice data to the PDF service and receives the PDF as a file, named after the invoice number. It also has an error path to Slack. The original notes say the same data always gives the same PDF; we did not verify that.
Archive to S3
Uploads the PDF to your bucket under the name invoices/INV-XXXXXXXX.pdf. It is set to continue even if it fails.
Log to Invoice Ledger
Adds a row to the Invoices tab: date, total (the amount Stripe charged, as a normal figure), invoice number, currency, customer (the “bill to” text) and payment ID. It runs after the S3 step whether or not the upload worked. You replace the placeholder YOUR_SPREADSHEET_URL. It is set to continue even if it fails.
Has Customer Email?
A yes-or-no node, running in parallel with the S3 step. It asks whether an email address is known. Yes goes to the email step, no goes to Slack.
Email Invoice to Customer
A Gmail node that sends the PDF as an attachment, with the subject “Invoice” followed by the number and the text “Thank you for your payment. Your invoice is attached.” It is a plain text email with no company name or signature. It is set to continue even if it fails.
Alert Billing Channel
A Slack node that posts to the channel #billing. Its text says “Invoice issue for Stripe payment” followed by the payment identifier, then either “render FAILED, no invoice produced” with the reason, or “no customer email on file; invoice archived to S3 but not sent.” If the invoice data could not be built, the payment identifier may not be available, and the text may show the word unknown. We could not confirm how n8n handles that case.
Get the workflow JSON
Open the box below to copy the JSON, or download the file. It contains the 12 nodes above, connected as described, with placeholders for your company name, bucket and spreadsheet, and six notes on the canvas. It contains none of your logins.
Please read this first: this JSON is free and provided as is. n8n versions and installed nodes differ from one person to another, so it may not run correctly on your side without adjustments. The PDFops node must be installed first. We cannot test it on every setup. That is why this article includes a long section, Problems you may meet and how to fix them. And if you would rather not deal with any of it, you can request a custom workflow and let us handle it.
Click to show or hide the workflow JSON
Click inside the box, select the whole text, copy it, then paste it onto an empty n8n canvas. If selecting is awkward on your device, use the download link above.
{
"name": "Create Invoice PDFs from Stripe Payments with n8n (free JSON)",
"nodes": [
{
"id": "note-overview",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-840,
-120
],
"parameters": {
"width": 560,
"height": 1120,
"content": "## Invoice PDFs from Stripe payments\n\nEach successful Stripe payment (event payment_intent.succeeded) above the minimum amount is turned into an invoice PDF through a community node, saved to an Amazon S3 bucket, emailed to the customer through Gmail when an address is known, and logged as a row in Google Sheets. Problems are posted to Slack. The invoice is built from the payment, not from a Stripe invoice.\n\n### How it works\n\n1. Payment Succeeded: Stripe trigger.\n2. Set Invoice Defaults: company name, invoice prefix, tax rate, default line text, notes, bucket name and minimum amount.\n3. Has Billable Amount?: payments below the minimum stop at Skip Zero-Amount Payment.\n4. Fetch Stripe Customer: looks up the customer for name and email.\n5. Build Invoice Payload: converts the amount, derives the net when a tax rate is set, builds the invoice data.\n6. PDFops Generate Invoice: renders the PDF.\n7. Archive to S3 saves it, then Log to Invoice Ledger adds a row.\n8. Has Customer Email?: yes goes to Email Invoice to Customer, no goes to Alert Billing Channel.\n\n### Setup\n\n1. Install the community node n8n-nodes-pdfops in n8n. Without it the file shows a missing node.\n2. Connect Stripe, AWS (S3), Gmail, Google Sheets and Slack credentials. Start with Stripe test mode.\n3. In Set Invoice Defaults, replace YOUR_COMPANY_NAME and YOUR_S3_BUCKET_NAME and check the other values. minimumAmount is in the currency's smallest unit.\n4. In Log to Invoice Ledger, replace YOUR_SPREADSHEET_URL. The Invoices tab needs the headers Date, Total, Invoice, Currency, Customer, Payment ID.\n5. In Alert Billing Channel, check the Slack channel.\n6. n8n must be reachable from the internet over HTTPS so that Stripe can call it.\n\n### Customize\n\n- Tax rate: taxRate in Set Invoice Defaults. Leave it at 0 if the invoice total must equal the charge exactly.\n- Storage: replace the S3 node with another storage node.\n\nSource: based on a template from the n8n Library at n8n.io, documented by LeenLabs.\nNot tested on live APIs."
},
"typeVersion": 1
},
{
"id": "note-section-1",
"name": "Section 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-160,
-40
],
"parameters": {
"color": 7,
"width": 700,
"height": 700,
"content": "## 1. Catch the payment\n\nThe Stripe trigger fires for every successful payment in the Stripe account of the credential. Set Invoice Defaults holds the settings. Payments below minimumAmount stop at Skip Zero-Amount Payment."
},
"typeVersion": 1
},
{
"id": "note-section-2",
"name": "Section 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
-40
],
"parameters": {
"color": 7,
"width": 440,
"height": 700,
"content": "## 2. Customer and invoice data\n\nFetch Stripe Customer looks up the customer. If that lookup fails, the workflow continues with the receipt email or the word Customer. Build Invoice Payload converts the amount and, if taxRate is above 0, derives the net from the charged total."
},
"typeVersion": 1
},
{
"id": "note-section-3",
"name": "Section 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1020,
-40
],
"parameters": {
"color": 7,
"width": 660,
"height": 700,
"content": "## 3. Render, store, send, log\n\nThe PDF is rendered, then saved to S3 and sent by email in parallel, and a ledger row is added. A failure in S3, Gmail or Sheets does not stop the workflow and does not post to Slack."
},
"typeVersion": 1
},
{
"id": "note-section-4",
"name": "Section 4",
"type": "n8n-nodes-base.stickyNote",
"position": [
960,
700
],
"parameters": {
"color": 7,
"width": 480,
"height": 280,
"content": "## Failure path\n\nA failure in Build Invoice Payload or in the PDF step, and a payment with no customer email, post a message to Slack. The text asks for the payment id; for a payload failure it may read unknown."
},
"typeVersion": 1
},
{
"id": "trigger-stripe",
"name": "Payment Succeeded",
"type": "n8n-nodes-base.stripeTrigger",
"position": [
-80,
180
],
"parameters": {
"events": [
"payment_intent.succeeded"
]
},
"typeVersion": 1
},
{
"id": "set-invoice-defaults",
"name": "Set Invoice Defaults",
"type": "n8n-nodes-base.set",
"position": [
140,
180
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-company",
"name": "companyName",
"type": "string",
"value": "YOUR_COMPANY_NAME"
},
{
"id": "cfg-prefix",
"name": "invoicePrefix",
"type": "string",
"value": "INV"
},
{
"id": "cfg-tax",
"name": "taxRate",
"type": "number",
"value": 0
},
{
"id": "cfg-line",
"name": "defaultLineItem",
"type": "string",
"value": "Payment received"
},
{
"id": "cfg-notes",
"name": "invoiceNotes",
"type": "string",
"value": "Thank you for your business."
},
{
"id": "cfg-bucket",
"name": "s3Bucket",
"type": "string",
"value": "YOUR_S3_BUCKET_NAME"
},
{
"id": "cfg-min",
"name": "minimumAmount",
"type": "number",
"value": 1
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "if-billable",
"name": "Has Billable Amount?",
"type": "n8n-nodes-base.if",
"position": [
360,
180
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-billable",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ ($json.data.object.amount_received || $json.data.object.amount || 0) >= $json.minimumAmount }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "noop-skip",
"name": "Skip Zero-Amount Payment",
"type": "n8n-nodes-base.noOp",
"position": [
360,
440
],
"parameters": {},
"typeVersion": 1
},
{
"id": "stripe-customer",
"name": "Fetch Stripe Customer",
"type": "n8n-nodes-base.stripe",
"onError": "continueRegularOutput",
"position": [
580,
180
],
"parameters": {
"resource": "customer",
"operation": "get",
"customerId": "={{ $json.data.object.customer }}"
},
"typeVersion": 1
},
{
"id": "code-build-invoice",
"name": "Build Invoice Payload",
"type": "n8n-nodes-base.code",
"onError": "continueErrorOutput",
"position": [
800,
180
],
"parameters": {
"jsCode": "// Stripe reports amounts in the smallest currency unit — but not every currency\n// has 100 of them. JPY, KRW and 13 others are zero-decimal, so the reflexive\n// `amount / 100` issues an invoice 100x too small. Check before dividing.\nconst ZERO_DECIMAL = ['bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf'];\n\nconst cfg = $('Set Invoice Defaults').first().json;\n// The Stripe Trigger emits the Event envelope, not the PaymentIntent —\n// the object itself is nested at data.object.\nconst payment = $('Payment Succeeded').first().json.data.object;\nconst customer = $input.first().json || {};\n\nconst currency = (payment.currency || 'usd').toLowerCase();\nconst zeroDecimal = ZERO_DECIMAL.includes(currency);\nconst decimals = zeroDecimal ? 0 : 2;\nconst round = (v) => Number(v.toFixed(decimals));\n\nconst total = round((payment.amount_received || payment.amount || 0) / (zeroDecimal ? 1 : 100));\n\n// If a tax rate is set, derive the net so the invoice shows net + tax. Note the\n// limit: PDFops recomputes subtotal -> tax -> total from unit_price, and roughly\n// 10% of (amount, rate) pairs have NO 2-decimal net that multiplies back to the\n// exact charged total — it is a representability limit, not a rounding bug.\n// Leave taxRate at 0 (the default) if you need the invoice total to equal the\n// charge exactly; the sticky says so too.\nconst rate = Number(cfg.taxRate) || 0;\nconst unitPrice = round(rate > 0 ? total / (1 + rate / 100) : total);\n\n// Guest checkouts have no customer object — fall back to the receipt email.\nconst billTo = customer.name || customer.email || payment.receipt_email || 'Customer';\n\nreturn [\n {\n json: {\n invoice: {\n from: cfg.companyName,\n to: billTo,\n invoice_number: `${cfg.invoicePrefix}-${String(payment.id).slice(-8).toUpperCase()}`,\n date: new Date((payment.created || Date.now() / 1000) * 1000).toISOString().slice(0, 10),\n currency: currency.toUpperCase(),\n tax_rate: rate,\n notes: cfg.invoiceNotes,\n items: [\n {\n description: payment.description || cfg.defaultLineItem,\n quantity: 1,\n unit_price: unitPrice,\n },\n ],\n },\n customerEmail: customer.email || payment.receipt_email || '',\n paymentId: payment.id,\n chargedTotal: total,\n s3Bucket: cfg.s3Bucket,\n },\n },\n];"
},
"typeVersion": 2
},
{
"id": "pdfops-invoice",
"name": "PDFops Generate Invoice",
"type": "n8n-nodes-pdfops.pdfops",
"onError": "continueErrorOutput",
"position": [
1040,
180
],
"parameters": {
"invoice": "={{ $json.invoice }}",
"fileName": "={{ $json.invoice.invoice_number }}.pdf",
"operation": "invoice",
"outputBinaryPropertyName": "data"
},
"typeVersion": 1
},
{
"id": "s3-upload",
"name": "Archive to S3",
"type": "n8n-nodes-base.awsS3",
"onError": "continueRegularOutput",
"position": [
1260,
180
],
"parameters": {
"fileName": "={{ 'invoices/' + $('Build Invoice Payload').item.json.invoice.invoice_number + '.pdf' }}",
"resource": "file",
"operation": "upload",
"binaryData": true,
"bucketName": "={{ $('Build Invoice Payload').item.json.s3Bucket }}",
"additionalFields": {},
"binaryPropertyName": "data"
},
"typeVersion": 2
},
{
"id": "gmail-invoice",
"name": "Email Invoice to Customer",
"type": "n8n-nodes-base.gmail",
"onError": "continueRegularOutput",
"position": [
1480,
400
],
"parameters": {
"sendTo": "={{ $('Build Invoice Payload').item.json.customerEmail }}",
"message": "Thank you for your payment. Your invoice is attached.",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{
"property": "data"
}
]
}
},
"subject": "={{ 'Invoice ' + $('Build Invoice Payload').item.json.invoice.invoice_number }}",
"resource": "message",
"emailType": "text",
"operation": "send"
},
"typeVersion": 2.1
},
{
"id": "sheets-ledger",
"name": "Log to Invoice Ledger",
"type": "n8n-nodes-base.googleSheets",
"onError": "continueRegularOutput",
"position": [
1480,
180
],
"parameters": {
"columns": {
"value": {
"Date": "={{ $('Build Invoice Payload').item.json.invoice.date }}",
"Total": "={{ $('Build Invoice Payload').item.json.chargedTotal }}",
"Invoice": "={{ $('Build Invoice Payload').item.json.invoice.invoice_number }}",
"Currency": "={{ $('Build Invoice Payload').item.json.invoice.currency }}",
"Customer": "={{ $('Build Invoice Payload').item.json.invoice.to }}",
"Payment ID": "={{ $('Build Invoice Payload').item.json.paymentId }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"mode": "name",
"value": "Invoices"
},
"documentId": {
"mode": "url",
"value": "YOUR_SPREADSHEET_URL"
}
},
"typeVersion": 4.5
},
{
"id": "slack-alert",
"name": "Alert Billing Channel",
"type": "n8n-nodes-base.slack",
"onError": "continueRegularOutput",
"position": [
1040,
820
],
"parameters": {
"text": "={{ 'Invoice issue for Stripe payment ' + ($('Build Invoice Payload').item.json.paymentId || 'unknown') + ' — ' + ($json.error ? ('render FAILED, no invoice produced: ' + ($json.error.message || $json.error)) : 'no customer email on file; invoice archived to S3 but not sent.') }}",
"select": "channel",
"resource": "message",
"channelId": {
"mode": "name",
"value": "#billing"
},
"operation": "post",
"otherOptions": {}
},
"typeVersion": 2.2
},
{
"id": "if-has-email",
"name": "Has Customer Email?",
"type": "n8n-nodes-base.if",
"position": [
1260,
400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-email",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ !!$('Build Invoice Payload').item.json.customerEmail }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "8e1f4a73-2d95-4b60-a7c8-5f0e3b91d246",
"name": "Before you turn it on",
"type": "n8n-nodes-base.stickyNote",
"position": [
-840,
-1280
],
"parameters": {
"color": 3,
"width": 560,
"height": 1100,
"content": "## Before you turn it on\n\n1. This creates and emails invoices on its own for every successful payment in the connected Stripe account, including payments that Stripe Billing or another tool already invoices. Check for double invoices.\n2. The invoice is built from the payment: one line, quantity 1, the description or the default text. No address, tax number or sequential invoice number. Check that it meets the invoice rules that apply to you.\n3. With taxRate above 0 the net is derived backwards from the charged total, so the PDF total can differ by a cent from the Stripe total. Prices that are tax-exclusive are not handled.\n4. Amounts are converted by a fixed list of currencies. Check your currencies, especially UGX, which Stripe documents as a two-decimal value.\n5. Stripe can deliver the same event more than once. There is no duplicate check, so you can get two emails and two ledger rows.\n6. Only Build Invoice Payload and PDF failures and missing emails post to Slack. A failure in S3, Gmail or Sheets is silent, and the ledger row is added anyway.\n7. The invoice number comes from the last 8 characters of the payment id, upper-cased. It is not sequential.\n8. Customer names and emails go to the PDF service, S3, Gmail and Sheets. Make the bucket private and read the terms of each service.\n9. Test with Stripe test mode and fake payments first.\n10. Not tested on live APIs."
},
"typeVersion": 1
}
],
"connections": {
"Archive to S3": {
"main": [
[
{
"node": "Log to Invoice Ledger",
"type": "main",
"index": 0
}
]
]
},
"Payment Succeeded": {
"main": [
[
{
"node": "Set Invoice Defaults",
"type": "main",
"index": 0
}
]
]
},
"Has Customer Email?": {
"main": [
[
{
"node": "Email Invoice to Customer",
"type": "main",
"index": 0
}
],
[
{
"node": "Alert Billing Channel",
"type": "main",
"index": 0
}
]
]
},
"Has Billable Amount?": {
"main": [
[
{
"node": "Fetch Stripe Customer",
"type": "main",
"index": 0
}
],
[
{
"node": "Skip Zero-Amount Payment",
"type": "main",
"index": 0
}
]
]
},
"Set Invoice Defaults": {
"main": [
[
{
"node": "Has Billable Amount?",
"type": "main",
"index": 0
}
]
]
},
"Build Invoice Payload": {
"main": [
[
{
"node": "PDFops Generate Invoice",
"type": "main",
"index": 0
}
],
[
{
"node": "Alert Billing Channel",
"type": "main",
"index": 0
}
]
]
},
"Fetch Stripe Customer": {
"main": [
[
{
"node": "Build Invoice Payload",
"type": "main",
"index": 0
}
]
]
},
"PDFops Generate Invoice": {
"main": [
[
{
"node": "Archive to S3",
"type": "main",
"index": 0
},
{
"node": "Has Customer Email?",
"type": "main",
"index": 0
}
],
[
{
"node": "Alert Billing Channel",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
Install it in fourteen steps
- Install the community node. In n8n, find the PDFops node (
n8n-nodes-pdfops) and install it. Where and how depends on your n8n. - Prepare the sheet. Create the Invoices tab with the six headers.
- Prepare S3. Create a private bucket and an access key that can write to it.
- Get the JSON from the box above or the download link.
- Import it. Open a new empty workflow in n8n, then paste the JSON onto the canvas or import it from a file.
- Check the 12 nodes appear, with six notes and no missing-node warning.
- Connect Stripe in Payment Succeeded and Fetch Stripe Customer, using test mode keys.
- Connect AWS in Archive to S3, Gmail in Email Invoice to Customer, Google Sheets in Log to Invoice Ledger and Slack in Alert Billing Channel.
- Fill in Set Invoice Defaults. Replace the company name and bucket placeholders, and keep the tax rate at 0 for the first tests.
- Fill in the sheet and Slack. Replace
YOUR_SPREADSHEET_URLand check the channel name. - Test. Turn the workflow on with test credentials and make a fake test payment in Stripe, with a customer email that is your own. Check the PDF, the S3 file, the email and the row.
- Test the odd cases. A guest payment without a customer, a payment with no email, a zero amount and a payment in a currency like yen.
- Decide what is invoiced. Check that Stripe’s own invoicing and your other tools do not already invoice the same payments.
- Go live carefully. Switch to live Stripe credentials only when you are happy, and check the first live invoices by hand.
What it will not do: limits, costs and trade-offs
- One line only. The invoice has a single line with quantity 1.
- No addresses or tax numbers, and no sequential numbering.
- No refunds or credit notes. It reacts to successful payments only.
- No duplicate check. A repeated event gives a second email and a second row.
- One flat tax rate, worked out from the total. Tax-exclusive pricing is not handled, and Stripe Tax data is not read.
- A fixed currency list in the code.
- Partial error reporting. Three of the delivery steps fail silently.
- It depends on an outside PDF service. If it is down or over its limit, invoices are not made, and Slack hears about it.
- It does not tell you about payments below the minimum.
- It needs an internet-reachable n8n, so it is not suited to a computer that is off or behind a home network.
Access, privacy and responsible use
This workflow handles payment records and customer details, and it creates documents that customers may rely on.
- Check whether the PDF is a valid invoice for you. Rules on what an invoice must contain, such as addresses, tax numbers and sequential numbers, differ by country and by customer. We are not an accountant or a tax adviser. This guide is not tax, legal or financial advice, and nothing in it means these PDFs meet any requirement.
- Know where customer data goes. Names, emails and amounts go to the PDF service, to S3, to Gmail and to Google Sheets. Read each provider’s terms, and tell your customers where their data goes if the rules that apply to you require it.
- Make the bucket private, and limit who can read it. Invoices contain personal data.
- Use least privilege. Give the AWS key permission only to write to that bucket, and give the Stripe key only the access n8n needs.
- Protect the ledger. Share the sheet only with people who need it.
- Keep your n8n address safe. Anyone who can reach the listener address could try to send fake events. Keep your n8n up to date, and check what Stripe does to sign its messages and whether the node verifies them. We did not verify that.
- Check the first invoices by hand, every currency you use and every tax setting.
Problems you may meet and how to fix them
Every n8n setup is different, so this workflow can behave differently for you than in our checks. Here are the most likely problems. For each one: what you see, why it happens, and what to do. They come from how the nodes are built and from running the Code node on simulated payments, not from a live run with real accounts.
1. The PDFops node shows as missing
What you see: a red or unknown node after import, or an error about an unknown node type.
Why: PDFops is a community node, and it is not part of a normal n8n installation.
Fix: install the community node first, then import the file again. If your n8n does not allow community nodes, ask us for a version that uses another PDF method.
2. Stripe will not connect, or the trigger does not register
What you see: an error in Payment Succeeded when you turn the workflow on.
Why: the key is wrong, restricted, or from a different mode, or Stripe cannot reach your n8n address.
Fix: check the key and its permissions, and make sure your n8n has a public HTTPS address set up correctly.
3. A payment is made but nothing happens
What you see: no run in the executions list.
Why: the workflow is not turned on, the payment was made in another mode than the key (test or live), or the event never reached n8n.
Fix: turn the workflow on, match the mode, and look at the webhook deliveries in your Stripe dashboard.
4. A payment is skipped
What you see: a run that ends at Skip Zero-Amount Payment.
Why: the amount is below minimumAmount, which is in the smallest unit of the currency.
Fix: lower the minimum, and remember that 500 is 5.00 in dollars.
5. Customers get two invoices
What you see: an invoice from this workflow and one from Stripe, or from another tool.
Why: the workflow reacts to every successful payment, including payments that other invoicing already covers.
Fix: decide which payments you invoice here, and add a filter for the others.
6. The same payment gives two emails and two rows
What you see: duplicate emails and ledger lines with the same payment ID.
Why: webhooks can deliver the same event more than once (this is general webhook behavior; check Stripe’s webhook documentation for the details), and nothing checks the payment ID against the ledger.
Fix: sort the ledger by payment ID to find duplicates, or ask us to add a check before the PDF step.
7. The invoice is addressed to “Customer” or to an email
What you see: no name on the invoice.
Why: the payment has no customer (a guest checkout), the customer has no name, or the customer lookup failed. That step is set to continue on failure, so a wrong key or a missing permission is hidden.
Fix: check the output of Fetch Stripe Customer for an error message.
8. An invoice is 100 times too large in some currency
What you see: 500 UGX instead of 5 UGX.
Why: the code’s list of zero-decimal currencies includes UGX, while Stripe’s currencies page says UGX is written as a two-decimal value (to charge 5 UGX, you provide 500).
Fix: remove ugx from the list in Build Invoice Payload if you accept it, and check any other currency you use against Stripe’s page.
9. Some other currency shows a wrong amount
What you see: an amount that is clearly 10 or 100 times off.
Why: the code assumes that any currency not in its list has two decimals. A currency with different rules would be converted wrongly.
Fix: test every currency you accept before going live, with a fake payment.
10. The invoice total is a cent off from the charge
What you see: 20.00 on the PDF, 19.99 charged.
Why: with a tax rate above 0, the PDF service recomputes the total from a net price and a rate, and some totals cannot be reproduced exactly.
Fix: leave taxRate at 0, or ask us for a version that shows the tax from Stripe.
11. The tax is wrong when your prices exclude tax
What you see: a net price that is lower than your list price.
Why: the workflow assumes that the charged total already includes tax, and works backwards.
Fix: if you add tax on top of your prices, do not use the tax rate here.
12. The PDF is not created
What you see: a Slack message “render FAILED, no invoice produced”.
Why: the PDF service is down, over its limit or refused the data. The original notes mention a limit without an account; we could not verify it.
Fix: read the reason in the message, check the provider’s limits and status, and create the invoice by hand for that payment.
13. The Slack message says “unknown” for the payment
What you see: “Invoice issue for Stripe payment unknown”.
Why: when the invoice data could not be built, the payment identifier is not available to the Slack step.
Fix: look at the failed run in the executions list to find the payment.
14. No Slack message, but the invoice is missing
What you see: a payment with no PDF in S3, no email, or no ledger row, and silence in Slack.
Why: the S3, Gmail and Google Sheets steps are set to continue on failure and are not connected to Slack.
Fix: compare Stripe’s payments with your ledger once a week, and ask us to add alerts to those steps.
15. The upload to S3 fails
What you see: no file in the bucket, and a ledger row anyway.
Why: the bucket name is still YOUR_S3_BUCKET_NAME, the key lacks permission, or the region does not match. The ledger step runs after S3 whether or not it worked.
Fix: check the name, permissions and region, and look at the node’s output.
16. The customer did not get the email
What you see: a customer says nothing arrived.
Why: Gmail failed silently, the email went to spam, or the address on the payment’s receipt differs from the one the customer reads. A personal Gmail account is also limited in how many emails it can send.
Fix: check your sent folder, send from a business address, and resend by hand.
17. The ledger row is empty or in the wrong place
What you see: blank cells or no row.
Why: the headers do not match exactly (Payment ID has a space), the tab is not named Invoices, or the placeholder spreadsheet address is still there.
Fix: fix the headers, the tab name and the address.
18. Two invoices have the same number
What you see: a PDF replaced in S3.
Why: the number is the last 8 characters of the payment identifier in capital letters. Two identifiers can share it, as in our test.
Fix: use a longer part of the identifier, or a real counter. Ask us for a version with sequential numbers.
19. The invoice date is a day off
What you see: the day before or after the customer’s own date.
Why: the date is taken in universal time.
Fix: accept it, or ask us to use your timezone.
20. Slack does not receive anything
What you see: no message, though the run shows the Slack step.
Why: the channel does not exist under that name, or the Slack app is not in it. This step also continues silently on failure.
Fix: check the channel name and add the app to the channel.
21. A refund leaves a live invoice
What you see: an invoice for a payment that was refunded.
Why: the workflow only reacts to successful payments.
Fix: handle refunds and credit notes by hand, or ask us to add them.
22. The invoice lacks details a customer asks for
What you see: a request for an address, a tax number or a purchase order.
Why: the invoice has only the fields the code prepares.
Fix: ask us for a version with the fields you need.
23. After import, nodes show missing credentials
What you see: warnings on the Stripe, AWS, Gmail, Google Sheets and Slack nodes.
Why: the file deliberately contains no credentials.
Fix: open each node and select or create your own credential.
Too much of a puzzle? Let us build or adapt it for you
Some people enjoy tinkering with n8n. Others want invoices that simply work with their own payments, currencies and rules. If you are in the second group, LeenLabs builds and adapts n8n workflows for people like you. We can:
- Prevent double invoices with a payment ID check and a filter for payments already invoiced elsewhere.
- Add alerts to the S3, Gmail and Google Sheets steps.
- Fix the currency conversion for the currencies you accept.
- Read tax from Stripe instead of working it out backwards.
- Add fields such as addresses, tax numbers and several lines.
- Use sequential numbers and handle refunds.
- Use another PDF method if you cannot install a community node.
- Build a new workflow from scratch for another task you want to automate.
Request a Custom Workflow, describe what you want to automate, and we will come back to you with the next steps. Prefer to ask a question first? Write to support@leenlabs.site.
What we changed in the original template
We want you to know exactly how this file differs from the template it comes from. Compared with the file we received, LeenLabs made these changes:
- Rewrote the five canvas notes. The original notes made claims we could not verify, such as how the PDF service behaves, its free quota and how the node is installed, and recommended it. We replaced them with plain explanations and a source credit, and stated that the file was not tested on live services.
- Made the placeholders clearer. The company name, the bucket name and the spreadsheet address now read
YOUR_COMPANY_NAME,YOUR_S3_BUCKET_NAMEandYOUR_SPREADSHEET_URL, so a forgotten replacement fails loudly. The original bucket name looked like a real name, and bucket names are shared across all of AWS. - Added a note called Before you turn it on with the main warnings.
- Removed internal data. We removed the template’s identifier, its setup-status marker and the inactive flag, and renamed the workflow to this article’s title.
The nodes, their settings, the error paths, the code and its comments, the formulas, the filters and the connections are unchanged.
What was verified, and what was not
Here is exactly what we did. We ran the Code node (Build Invoice Payload) outside n8n on 17 simulated payments: dollars, yen and won, a Ugandan shilling, an Icelandic króna and a three-decimal currency, a tax rate of 20 and one of 7.5, a guest payment with a failed customer lookup, a guest payment with no email at all, a customer without a name, a missing currency, a custom description, an amount received of 0, two identifiers that differ only in capitals, and a missing creation time. We evaluated the amount filter on four cases. We also checked Stripe’s currencies page for the rule on UGX and ISK, and simulated the tax rounding under the assumed rule described above.
We did not import this exact file into n8n and run it end to end. We did not install or run the PDFops node, so we know nothing about its output, its limits or its rounding. We did not connect Stripe, S3, Gmail, Google Sheets or Slack. So we cannot confirm how the error paths behave, whether the Slack text can read the payment identifier after a payload failure, how the customer lookup behaves for a guest payment in your version, whether the node verifies Stripe’s signatures, or how the Google Sheets step maps the columns. That is why we recommend a full test with Stripe test mode before you turn it on.
Five ways to adapt the workflow
- Change the company details. Edit Set Invoice Defaults.
- Rewrite the email. Edit the text and subject in Email Invoice to Customer, and add your company name.
- Change where PDFs go. Replace Archive to S3 with another storage node.
- Drop the ledger or Slack. Remove the node and its connection if you do not need it. This changes what the next steps receive, so test it.
- Fix the currency list. Edit the list at the top of Build Invoice Payload to match the currencies you accept and what Stripe documents for them.
These variations are suggestions based on how n8n works, not changes we tested on this template. If you want one built for you, send us a request.
Frequently asked questions
Is this a replacement for Stripe’s own invoices?
No. It makes a simple PDF from a payment. If you need numbering, addresses, tax details or subscriptions, use Stripe’s invoicing or accounting software.
Does it handle yen and other currencies without cents?
Yes for most of the ones on its list, but we found that UGX is on the list although Stripe documents it differently. Check every currency you use.
Is the invoice legally valid?
We cannot say. Invoice requirements depend on your country and your customer. Ask an accountant.
Does it use AI?
No. It uses fixed rules written in code.
Where does customer data go?
To the PDF service, Amazon S3, Gmail and Google Sheets. Read their terms.
Is the workflow free?
The file is free. The services it connects have their own plans, limits and prices.
What if the JSON does not work on my n8n?
It can happen, because setups differ. Go through the problems section above. If it still does not work, or you do not want to spend the time, request a custom workflow and we will handle it.
Who built the original template?
The original template comes from the n8n Library at n8n.io. LeenLabs adjusted and documented it.
Tools used in this workflow
The links below go to each tool’s home page, and some may be affiliate links, as explained in our Terms of Service. We do not link to PDFops because we could not verify its home page.
- n8n (may be an affiliate link): the automation platform that runs the workflow, available hosted or self-hosted.
- Stripe (may be an affiliate link): the payment platform that sends the payment events.
- Amazon S3 (may be an affiliate link): cloud storage for the invoice PDFs.
- Gmail (may be an affiliate link): the email service that sends the invoices.
- Google Sheets (may be an affiliate link): the spreadsheet that holds the ledger.
- Slack (may be an affiliate link): the chat tool that receives the problem messages.
Suggested reading
If invoices are on your mind, these LeenLabs workflow guides pair well with this one:
- Vendor invoice 3-way match with n8n: checks vendor invoices against purchase orders and goods receipts, with a duplicate check.
- Send weekly overdue invoice reminders with n8n: emails clients about unpaid invoices every Monday and marks them as chased.
- Extract invoice data from PDFs with n8n and Claude: reads invoice PDFs and logs the data, with a review queue for doubtful results.
Keep exploring
Browse more free n8n workflows from LeenLabs.
Need this adapted to your own setup?
Tell us what you want to automate and we will come back to you with the next steps.