{
  "name": "Track Pennylane invoice payment status with Slack notifications",
  "nodes": [
    {
      "id": "6ea2a45d-df0a-4ec7-a38e-740c40782606",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        -272
      ],
      "parameters": {
        "width": 640,
        "height": 1024,
        "content": "# Track Pennylane invoice payment status with Slack notifications\n\n## Who is this for\n\nTeams using Pennylane who want automatic alerts when invoices are paid or become overdue, without checking Pennylane manually.\n\n## What it does\n\nThis workflow runs every 15 minutes, fetches all customer invoices from Pennylane, and classifies them as paid, overdue, or upcoming. If any invoices have been paid or are overdue, it sends a summary notification to Slack. If everything is normal, the workflow ends silently.\n\n## How to set up\n\n1. Import this workflow into n8n\n2. Create a Header Auth credential: name `Authorization`, value `Bearer <YOUR_PENNYLANE_TOKEN>`\n3. Select your Slack channel in the SL Send Notification node\n4. Adjust the schedule interval in the Schedule Trigger node if needed\n\n## Requirements\n\n- Pennylane account with API access (Essentiel plan or higher)\n- Pennylane API token with scope: customer_invoices:all\n- (Optional) Slack workspace\n\nNote: Pennylane API does not support filtering by status or paid fields. All classification is done client-side in a Code node.\n\nPart of a 3-workflow billing suite: https://github.com/Gauthier-Huguenin/n8n-pennylane-auto-invoicing\n\n---\n\n### Flow\n\n`Schedule Trigger`\n  \u2192 `PL Fetch Invoices`\n  \u2192 `Code Filter Status Changes`\n  \u2192 `IF Has Updates`\n    \u2192 \u2705 `Code Build Payment Notification` \u2192 `SL Send Notification`\n    \u2192 \u274c `Set Done`\n\n---\n\n### Node refs for $()\n\n- Schedule Trigger\n- PL Fetch Invoices\n- Code Filter Status Changes\n- IF Has Updates\n- Code Build Payment Notification\n- SL Send Notification\n- Set Done"
      },
      "typeVersion": 1
    },
    {
      "id": "d45398dc-a8b8-4c63-a872-ea829ecfc92c",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -16,
        32
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "3472cf87-8215-4bd0-9e6d-fa78ecaed39a",
      "name": "PL Fetch Invoices",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        256,
        32
      ],
      "parameters": {
        "url": "https://app.pennylane.com/api/external/v2/customer_invoices",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": []
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "d2b7e251-96c3-4228-b08c-2c147dcda8b7",
      "name": "Code Filter Status Changes",
      "type": "n8n-nodes-base.code",
      "position": [
        496,
        32
      ],
      "parameters": {
        "jsCode": "// Refs: 'PL Fetch Invoices'\nconst invoices = $input.first().json.items;\nconst today = new Date().toISOString().split('T')[0];\n\nconst results = {\n  paid: [],\n  overdue: [],\n  upcoming: []\n};\n\nfor (const inv of invoices) {\n  if (inv.draft) continue;\n\n  if (inv.paid) {\n    results.paid.push(inv);\n  } else if (inv.deadline < today) {\n    results.overdue.push(inv);\n  } else {\n    results.upcoming.push(inv);\n  }\n}\n\nreturn [{\n  json: {\n    total_invoices: invoices.length,\n    paid_count: results.paid.length,\n    overdue_count: results.overdue.length,\n    upcoming_count: results.upcoming.length,\n    paid: results.paid,\n    overdue: results.overdue,\n    has_updates: results.paid.length > 0 || results.overdue.length > 0\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "023f3a34-1a30-4ab2-b8f4-14227dac655c",
      "name": "IF Has Updates",
      "type": "n8n-nodes-base.if",
      "position": [
        672,
        32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f6d3fe78-bb28-4193-b4c0-7d04e963d48f",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.has_updates }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "ac4d9578-3a5b-43cc-ab0a-8673987a377a",
      "name": "Set Done",
      "type": "n8n-nodes-base.set",
      "position": [
        1504,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "aee99e96-e05f-4770-b8c4-53a574358d7b",
              "name": "Status",
              "type": "string",
              "value": "no updates"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "66da8691-e7a6-4c3d-92f6-701fa2fdcc18",
      "name": "Code Build Payment Notification",
      "type": "n8n-nodes-base.code",
      "position": [
        912,
        -48
      ],
      "parameters": {
        "jsCode": "// Refs: 'Code Filter Status Changes'\nconst data = $input.first().json;\nconst lines = ['\ud83d\udcca Pennylane Invoice Status Report', ''];\n\nif (data.paid_count > 0) {\n  lines.push(`\u2705 ${data.paid_count} invoice(s) paid:`);\n  for (const inv of data.paid) {\n    lines.push(`  - ${inv.invoice_number} | ${inv.currency_amount} ${inv.currency} | ${inv.label}`);\n  }\n  lines.push('');\n}\n\nif (data.overdue_count > 0) {\n  lines.push(`\u26a0\ufe0f ${data.overdue_count} invoice(s) overdue:`);\n  for (const inv of data.overdue) {\n    lines.push(`  - ${inv.invoice_number} | ${inv.currency_amount} ${inv.currency} | Due: ${inv.deadline} | ${inv.label}`);\n  }\n  lines.push('');\n}\n\nlines.push(`\ud83d\udccb ${data.upcoming_count} invoice(s) upcoming`);\nlines.push(`Total tracked: ${data.total_invoices}`);\n\nreturn [{\n  json: {\n    message: lines.join('\\n'),\n    paid_count: data.paid_count,\n    overdue_count: data.overdue_count,\n    upcoming_count: data.upcoming_count\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8284d459-6b14-428c-84c2-208511b97546",
      "name": "SL Send Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        1152,
        -48
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": ""
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "8110087c-5802-4805-bedb-151a9261bb02",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        -272
      ],
      "parameters": {
        "color": 6,
        "width": 272,
        "height": 560,
        "content": "## 1. Trigger\n\nRuns every 15 minutes.\nPolls Pennylane for all customer invoices.\n\nAdjust the schedule interval in the\nSchedule Trigger node to fit your needs."
      },
      "typeVersion": 1
    },
    {
      "id": "07df7bfc-ded9-4119-b697-706ed1710194",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        -272
      ],
      "parameters": {
        "color": 3,
        "width": 272,
        "height": 560,
        "content": "## 2. Fetch invoices\n\nRetrieves all customer invoices from Pennylane.\n\nEndpoint: GET /customer_invoices\n\nNote: the Pennylane API does not support\nfiltering by status or paid fields.\nAll filtering is done in the next Code node."
      },
      "typeVersion": 1
    },
    {
      "id": "f4e10525-59ed-46a2-ad6b-1b2b8e4bb789",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -272
      ],
      "parameters": {
        "color": 3,
        "width": 416,
        "height": 560,
        "content": "## 3. Classify & filter\n\nClassifies each invoice into 3 categories:\n- paid: invoice has been settled\n- overdue: past deadline and still unpaid\n- upcoming: not yet due\n\nOnly triggers a notification if there are\npaid or overdue invoices (has_updates = true).\n\nIf all invoices are upcoming, the workflow\nends silently via Set Done."
      },
      "typeVersion": 1
    },
    {
      "id": "ee1f614f-fc39-4d68-b1e6-74dc303b0297",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        -272
      ],
      "parameters": {
        "color": 4,
        "width": 512,
        "height": 560,
        "content": "## 4. Notify\n\nBuilds a summary message with:\n- List of newly paid invoices\n- List of overdue invoices\n- Count of upcoming invoices\n\nSends to Slack. Replace with your preferred\nnotification channel (Telegram, Email, etc.)."
      },
      "typeVersion": 1
    },
    {
      "id": "4f5a9ddb-f537-4282-8c6a-47a3b36f30b5",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        -272
      ],
      "parameters": {
        "color": 4,
        "width": 368,
        "height": 560,
        "content": "## 5. Done\n\nNo paid or overdue invoices detected.\nWorkflow ends silently.\n\nThis node ensures a clean execution log\nin n8n. No notification is sent."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "connections": {
    "IF Has Updates": {
      "main": [
        [
          {
            "node": "Code Build Payment Notification",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "PL Fetch Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PL Fetch Invoices": {
      "main": [
        [
          {
            "node": "Code Filter Status Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Filter Status Changes": {
      "main": [
        [
          {
            "node": "IF Has Updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Build Payment Notification": {
      "main": [
        [
          {
            "node": "SL Send Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}