Skip to content

In n8n, copy this file and paste it onto the canvas, or use Import from File.

{
  "name": "21 · Overdue Invoice Chaser (starter)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1-5"
            }
          ]
        }
      },
      "id": "m21-weekdays-9-am",
      "name": "Weekdays 9 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ar/run",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m21-chase-run-api",
      "name": "Chase Run API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        180
      ],
      "webhookId": "course-21-run"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ar/pause",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m21-pause-api",
      "name": "Pause API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        400
      ],
      "webhookId": "course-21-pause"
    },
    {
      "parameters": {
        "httpMethod": "GET",
        "path": "ar/status",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m21-status-api",
      "name": "Status API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        580
      ],
      "webhookId": "course-21-status"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "business_name",
              "value": "Northwind Studio",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "from_name",
              "value": "Ava Chen",
              "type": "string"
            },
            {
              "id": "s3",
              "name": "owner_email",
              "value": "owner@northwind.example",
              "type": "string"
            },
            {
              "id": "s4",
              "name": "currency",
              "value": "usd",
              "type": "string"
            },
            {
              "id": "s5",
              "name": "timezone",
              "value": "America/New_York",
              "type": "string"
            },
            {
              "id": "s6",
              "name": "stages",
              "value": "={{ [ { key: 'upcoming', day: -3 }, { key: 'due', day: 0 }, { key: 'overdue_1', day: 3 }, { key: 'overdue_2', day: 10 }, { key: 'final', day: 21 }, { key: 'escalate', day: 30 } ] }}",
              "type": "array"
            },
            {
              "id": "s7",
              "name": "min_gap_days",
              "value": 3,
              "type": "number"
            },
            {
              "id": "s8",
              "name": "min_amount",
              "value": 5,
              "type": "number"
            },
            {
              "id": "s9",
              "name": "do_not_chase",
              "value": "={{ ['bigretail.example', 'friend@example.com'] }}",
              "type": "array"
            },
            {
              "id": "s10",
              "name": "pay_instructions",
              "value": "Bank transfer details are on the invoice; reply to this email if you need them again.",
              "type": "string"
            },
            {
              "id": "s11",
              "name": "final_note",
              "value": "after which we may pause ongoing work",
              "type": "string"
            },
            {
              "id": "s12",
              "name": "summary_day",
              "value": "Mon",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "m21-settings-edit-me",
      "name": "Settings (edit me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        280
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "r1",
                    "leftValue": "={{ $('Weekdays 9 AM').isExecuted || $('Chase Run API').isExecuted }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "run"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "r2",
                    "leftValue": "={{ $('Pause API').isExecuted }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "pause"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "r3",
                    "leftValue": "={{ $('Status API').isExecuted }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "status"
            }
          ]
        },
        "options": {}
      },
      "id": "m21-route",
      "name": "Route",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        460,
        280
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $('Chase Run API').isExecuted && $('Chase Run API').first().json.body.source !== 'stripe' }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "m21-invoices-from-api",
      "name": "Invoices from API?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        700,
        60
      ]
    },
    {
      "parameters": {
        "url": "https://api.stripe.com/v1/invoices",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "stripeApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "status",
              "value": "open"
            },
            {
              "name": "collection_method",
              "value": "send_invoice"
            },
            {
              "name": "limit",
              "value": "100"
            }
          ]
        },
        "options": {}
      },
      "id": "m21-get-stripe-invoices",
      "name": "Get Stripe Invoices",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        920,
        160
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// One clean invoice list, whatever the source: simple JSON from the API, raw Stripe invoices\n// from the API (for testing the mapping), or the Stripe REST response (GET /v1/invoices).\nconst viaApi = $('Chase Run API').isExecuted;\nconst body = viaApi ? $('Chase Run API').first().json.body ?? {} : {};\nconst fromStripeHttp = $('Get Stripe Invoices').isExecuted;\nconst http = fromStripeHttp ? $('Get Stripe Invoices').first().json : null;\n\nconst fromStripe = (inv) => ({\n  invoice_id: inv.id,\n  number: inv.number ?? inv.id,\n  customer: inv.customer_name ?? inv.customer_email ?? 'customer',\n  email: (inv.customer_email ?? '').toLowerCase(),\n  amount_remaining: (inv.amount_remaining ?? inv.amount_due ?? 0) / 100, // Stripe amounts are in cents\n  currency: (inv.currency ?? 'usd').toLowerCase(),\n  due_date: inv.due_date ? new Date(inv.due_date * 1000).toISOString().slice(0, 10) : null,\n  status: inv.status,\n  pay_url: inv.hosted_invoice_url ?? '',\n  source: 'stripe',\n});\nconst fromSimple = (inv) => ({\n  invoice_id: String(inv.id ?? inv.invoice_id ?? ''),\n  number: String(inv.number ?? inv.id ?? ''),\n  customer: inv.customer ?? inv.email ?? 'customer',\n  email: String(inv.email ?? '').trim().toLowerCase(),\n  amount_remaining: Number(inv.amount_remaining ?? inv.amount ?? 0),\n  currency: String(inv.currency ?? $('Settings (edit me)').first().json.currency).toLowerCase(),\n  due_date: inv.due_date ?? null,\n  status: inv.status ?? 'open',\n  pay_url: inv.pay_url ?? '',\n  source: 'api',\n});\n\nlet invoices = [], source_error = null, has_more = false;\nif (fromStripeHttp) {\n  if (Array.isArray(http.data)) { invoices = http.data.map(fromStripe); has_more = Boolean(http.has_more); }\n  else source_error = String(http.error?.message ?? http.error ?? 'Stripe returned no invoice list (check the Stripe credential)').slice(0, 300);\n} else {\n  invoices = [...(body.invoices ?? []).map(fromSimple), ...(body.stripe_invoices ?? []).map(fromStripe)];\n}\n\nreturn [{\n  json: {\n    invoices: invoices.filter((i) => i.invoice_id),\n    source: fromStripeHttp ? 'stripe' : 'api',\n    source_error, has_more,\n    now: body.now || new Date().toISOString(), // tests inject the clock\n    dry_run: body.dry_run === true,\n    record_only: body.record_only === true, // mark reminders as sent without emailing (first-time import, or already chased by hand)\n    force_summary: body.summary === true,\n    via_api: viaApi,\n  },\n}];\n"
      },
      "id": "m21-normalize-invoices",
      "name": "Normalize Invoices",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1140,
        60
      ]
    },
    {
      "parameters": {
        "content": "## Your task\nInvoices are loaded and normalized (try a dry run with `invoices` in the body).\n1. **Plan Reminders** (Code): days past due in your timezone → latest due stage only (catch-up), skip paid/small/no date/do-not-chase/paused, 3-day gap, final notice before escalation; compose each email.\n2. **Send Now?** → **Gmail** (On Error: Continue).\n3. **Record and Summarize**: mark a stage sent only with a Gmail id; aging buckets + summary text.\n4. Respond (API) and email the A/R summary on Mondays.\n5. Pause and status endpoints.\n\nTest: `./test/test.sh`",
        "height": 260,
        "width": 420
      },
      "id": "starter-task-note",
      "name": "Your Task",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -340
      ]
    }
  ],
  "connections": {
    "Weekdays 9 AM": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chase Run API": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pause API": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Status API": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings (edit me)": {
      "main": [
        [
          {
            "node": "Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route": {
      "main": [
        [
          {
            "node": "Invoices from API?",
            "type": "main",
            "index": 0
          }
        ],
        [],
        []
      ]
    },
    "Invoices from API?": {
      "main": [
        [
          {
            "node": "Normalize Invoices",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Stripe Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Stripe Invoices": {
      "main": [
        [
          {
            "node": "Normalize Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Invoices": {
      "main": [
        []
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}