{
  "name": "Verify Polish VAT taxpayers (Biała lista) from Google Sheets",
  "nodes": [
    {
      "parameters": {
        "content": "## Verify Polish VAT taxpayers (Biała lista) from Google Sheets\n\n### How it works\n\nThis workflow verifies Polish VAT taxpayers from a Google Sheets contractor list against the Ministry of Finance Biała lista API. It can be started manually or on a weekday schedule, reads contractor data, normalizes and validates NIP numbers, calls the API, and turns the response into a plain verdict.\n\n### Setup steps\n\n- Configure Google Sheets credentials for the Read contractors node and select the spreadsheet/range containing contractor data.\n- Update the Settings node with the correct whiteListApi endpoint and the column names for NIP, contractor name, and bank account.\n- Confirm the weekday schedule time and timezone on the Every weekday 7:00 trigger, or use the manual trigger for ad hoc checks.\n- Review the Prepare NIPs and Evaluate results code nodes if your sheet uses different formatting or you need different output fields.\n\n### Customization\n\nAdjust the Settings node to point to different sheet columns or API environments, change the schedule frequency, or extend the evaluation code to write results back to Google Sheets, send alerts, or include bank-account matching.",
        "height": 864,
        "width": 480
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        240,
        -160
      ],
      "id": "f28aad5c-d64d-4104-8af1-d71887ccd195",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Start and configure\n\nContains the manual and weekday scheduled triggers, then centralizes runtime settings such as the Biała lista API URL and spreadsheet column names.",
        "height": 496,
        "width": 416,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        800,
        -160
      ],
      "id": "58b29a33-d9ee-4ca4-b06c-418026fd00eb",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Read and prepare NIPs\n\nReads contractor rows from Google Sheets and normalizes the NIP values from the configured column, including checksum validation before lookup.",
        "height": 336,
        "width": 416,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1248,
        -80
      ],
      "id": "3f707231-6f1b-4b48-ae65-8f83f4bc5f2b",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "## Check and evaluate taxpayers\n\nQueries the Ministry of Finance white list API for each prepared NIP and converts the response into a clear taxpayer verification verdict.",
        "height": 320,
        "width": 432,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1712,
        -64
      ],
      "id": "4d4efb7c-b9d6-4913-b85e-8209ef87db48",
      "name": "Sticky Note3"
    },
    {
      "parameters": {},
      "id": "1e04ee2a-f68a-4b4f-9af6-f0cc866ded05",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        864,
        16
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      },
      "id": "915efa21-7453-4a57-8766-07401849272b",
      "name": "Schedule Weekdays at 7am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        864,
        176
      ],
      "typeVersion": 1.2
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "0",
              "name": "whiteListApi",
              "type": "string",
              "value": "https://wl-api.mf.gov.pl/api/search/nip/"
            },
            {
              "id": "1",
              "name": "nipColumn",
              "type": "string",
              "value": "NIP"
            },
            {
              "id": "2",
              "name": "nameColumn",
              "type": "string",
              "value": "Company"
            },
            {
              "id": "3",
              "name": "accountColumn",
              "type": "string",
              "value": "Bank account"
            }
          ]
        },
        "options": {}
      },
      "id": "12cde684-d93a-4eb3-ae19-252a2575b98c",
      "name": "Set API and Columns",
      "type": "n8n-nodes-base.set",
      "position": [
        1072,
        96
      ],
      "typeVersion": 3.4
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "id": "d5dceeb9-c10c-426e-a4a5-9790056e95db",
      "name": "Read Contractors from Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1296,
        96
      ],
      "typeVersion": 4.5,
      "notes": "Select your spreadsheet and worksheet here."
    },
    {
      "parameters": {
        "jsCode": "// Normalise NIP numbers coming from the spreadsheet and validate the checksum.\n// A malformed NIP is rejected locally, so we never waste an API call on it.\nconst cfg = $('Set API and Columns').first().json;\nconst column = cfg.nipColumn || 'NIP';\n\nfunction checksumOk(nip) {\n  if (!/^\\d{10}$/.test(nip)) return false;\n  const w = [6, 5, 7, 2, 3, 4, 5, 6, 7];\n  const sum = w.reduce((acc, weight, i) => acc + weight * Number(nip[i]), 0);\n  return sum % 11 === Number(nip[9]);\n}\n\nconst out = [];\nfor (const item of $input.all()) {\n  const row = item.json;\n  const raw = String(row[column] ?? '').replace(/[^0-9]/g, '');\n  out.push({\n    json: {\n      ...row,\n      nip: raw,\n      nip_valid_format: checksumOk(raw),\n      row_number: row.row_number ?? null,\n    },\n  });\n}\nreturn out;"
      },
      "id": "bc02ed65-7f8e-4f79-884e-9f6935ca1e47",
      "name": "Normalize and Validate NIPs",
      "type": "n8n-nodes-base.code",
      "position": [
        1520,
        96
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "url": "={{ $('Set API and Columns').first().json.whiteListApi }}{{ $json.nip }}?date={{ $now.format('yyyy-MM-dd') }}",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 1200
            }
          },
          "timeout": 20000
        }
      },
      "id": "69059b30-1e02-4d5f-8f7e-d3c301ab7b1a",
      "name": "Fetch VAT White List Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1760,
        96
      ],
      "typeVersion": 4.2,
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Turn the Ministry of Finance response into a plain verdict.\n// The API answers with { result: { subject: {...} } } — an ACTIVE taxpayer has\n// statusVat = \"Czynny\". Anything else means: do not pay yet.\nconst cfg = $('Set API and Columns').first().json;\nconst inputs = $('Normalize and Validate NIPs').all().map(i => i.json);\nconst results = $input.all().map(i => i.json);\n\nconst rows = [];\nfor (let i = 0; i < inputs.length; i++) {\n  const src = inputs[i];\n  const res = results[i] || {};\n  const subject = res?.result?.subject || null;\n\n  let verdict, detail;\n  if (!src.nip_valid_format) {\n    verdict = 'INVALID_NIP';\n    detail = 'Checksum failed — the number is not a valid Polish NIP.';\n  } else if (!subject) {\n    verdict = 'NOT_FOUND';\n    detail = 'No taxpayer found for this NIP on the given date.';\n  } else if (subject.statusVat === 'Czynny') {\n    verdict = 'ACTIVE';\n    detail = subject.name || '';\n  } else {\n    verdict = 'NOT_ACTIVE';\n    detail = `statusVat = ${subject.statusVat || 'unknown'}`;\n  }\n\n  const accounts = Array.isArray(subject?.accountNumbers) ? subject.accountNumbers : [];\n  const expected = String(src[cfg.accountColumn || 'Bank account'] || '').replace(/[^0-9]/g, '');\n  const accountOk = !expected ? null : accounts.some(a => String(a).replace(/[^0-9]/g, '') === expected);\n\n  rows.push({ json: {\n    nip: src.nip,\n    company: subject?.name || src[cfg.nameColumn || 'Company'] || '',\n    verdict,\n    detail,\n    account_on_white_list: accountOk,\n    registration_legal_date: subject?.registrationLegalDate || null,\n    checked_at: new Date().toISOString(),\n    row_number: src.row_number,\n  }});\n}\n\nconst problems = rows.filter(r => r.json.verdict !== 'ACTIVE' || r.json.account_on_white_list === false);\nif (problems.length) {\n  rows.unshift({ json: {\n    summary: true,\n    checked: rows.length,\n    problems: problems.length,\n    message: `${problems.length} of ${rows.length} contractors need attention before payment.`,\n  }});\n}\nreturn rows;"
      },
      "id": "025266f5-e27f-4813-9a47-fff5027ac38e",
      "name": "Determine VAT Compliance Status",
      "type": "n8n-nodes-base.code",
      "position": [
        2000,
        96
      ],
      "typeVersion": 2
    }
  ],
  "pinData": {},
  "connections": {
    "Set API and Columns": {
      "main": [
        [
          {
            "node": "Read Contractors from Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize and Validate NIPs": {
      "main": [
        [
          {
            "node": "Fetch VAT White List Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Set API and Columns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch VAT White List Status": {
      "main": [
        [
          {
            "node": "Determine VAT Compliance Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Contractors from Sheets": {
      "main": [
        [
          {
            "node": "Normalize and Validate NIPs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Weekdays at 7am": {
      "main": [
        [
          {
            "node": "Set API and Columns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "meta": {},
  "nodeGroups": []
}
