AutomationFlowsData & Sheets › Download Ksef (poland’s E-invoicing System) Invoices to an Excel Spreadsheet

Download Ksef (poland’s E-invoicing System) Invoices to an Excel Spreadsheet

ByGreg Brzezinka @ai-greg on n8n.io

Download invoices from Poland's KSeF (Krajowy System e-Faktur) and export them as an XLSX spreadsheet. Handles the full v2 authentication flow automatically.

Event trigger★★★★☆ complexity18 nodesHTTP RequestSpreadsheet File
Data & Sheets Trigger: Event Nodes: 18 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #13925 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "nS78CfnstbrfE7Ag",
  "name": "KSeF - Download Invoices to Spreadsheet",
  "tags": [],
  "nodes": [
    {
      "id": "s1",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -768
      ],
      "parameters": {
        "color": 4,
        "width": 1048,
        "height": 442,
        "content": "## \ud83c\uddf5\ud83c\uddf1 KSeF \u2014 Download Invoices to Spreadsheet\n\nDownloads invoice metadata from Poland's **KSeF** (Krajowy System e-Faktur) and exports it as an **XLSX spreadsheet**.\n\n### Quick Start\n1. Open the **\u2699\ufe0f Config** node and fill in your **NIP** and **KSeF token**\n2. Set the **date range** (startDate / endDate)\n3. Click **Test workflow**\n4. Your spreadsheet will appear in the **Write XLSX** node output\n\n### How to get a KSeF token\nGenerate an authorization token at [ksef.mf.gov.pl](https://ksef.mf.gov.pl) \u2192 Log in \u2192 Manage tokens.\nTokens look like: `YYYYMMDD-XX-XXXXXXXXXX-XXXXXXXXXX-XX|nip-XXXXXXXXXX|hash`\n\n### Need help?\nKSeF docs: https://www.gov.pl/web/kas/krajowy-system-e-faktur\n\nMade with \u2764\ufe0f by [Greg Brzezinka](greg@prosit.no) Need help? [Reach out to me](https://www.linkedin.com/in/brzezinka)!"
      },
      "typeVersion": 1
    },
    {
      "id": "s2",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -272
      ],
      "parameters": {
        "width": 464,
        "height": 520,
        "content": "## \ud83d\udd27 Configuration\nEdit the JSON below to set your:\n- **nip** \u2014 your 10-digit NIP number\n- **authToken** \u2014 KSeF authorization token\n- **startDate / endDate** \u2014 ISO 8601 format\n- **subjectType** \u2014\n  `Subject2` = invoices you **received** (buyer)\n  `Subject1` = invoices you **issued** (seller)\n\nDates use `PermanentStorage` type (when KSeF stored the invoice)."
      },
      "typeVersion": 1
    },
    {
      "id": "s3",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -272
      ],
      "parameters": {
        "color": 3,
        "width": 1884,
        "height": 520,
        "content": "## \ud83d\udd10 Authentication Flow (v2 API)\nKSeF uses a multi-step auth:\n1. **Get Public Key** \u2014 fetch RSA certificate\n2. **Get Challenge** \u2014 get a challenge + timestamp\n3. **Encrypt Token** \u2014 RSA-OAEP encrypt `token|timestamp`\n4. **Init Auth** \u2014 submit encrypted token, get temp JWT\n5. **Wait + Check Status** \u2014 poll until auth is ready\n6. **Redeem Token** \u2014 exchange temp JWT for access token\n\n\u26a0\ufe0f The `authenticationToken` from step 4 is **temporary**!\nOnly the `accessToken` from step 6 works for API calls."
      },
      "typeVersion": 1
    },
    {
      "id": "s4",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2352,
        -272
      ],
      "parameters": {
        "color": 2,
        "width": 484,
        "height": 522,
        "content": "## \ud83d\udcca Output\nInvoice metadata is flattened into columns:\nKSeF Number, Invoice Number, Issue Date, Seller/Buyer NIP & Name, Net/VAT/Gross amounts, Currency, Type.\n\nThe XLSX file is available as binary output in the **Write XLSX** node.\n\nTo save to disk, connect a **Write Binary File** node after Write XLSX.\nTo email it, connect a **Send Email** node and attach the binary.\n\nSwap it to Google Spreadsheet or database of your choice."
      },
      "typeVersion": 1
    },
    {
      "id": "n1",
      "name": "When clicking 'Test workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "n2",
      "name": "\u2699\ufe0f Config",
      "type": "n8n-nodes-base.set",
      "position": [
        224,
        0
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "{\n  \"baseUrl\": \"https://api.ksef.mf.gov.pl/v2\",\n  \"nip\": \"YOUR_NIP_HERE\",\n  \"authToken\": \"YOUR_KSEF_TOKEN_HERE\",\n  \"startDate\": \"2026-02-01T00:00:00Z\",\n  \"endDate\": \"2026-03-06T23:59:59Z\",\n  \"subjectType\": \"Subject2\"\n}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "n3",
      "name": "Get Public Key",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        448,
        0
      ],
      "parameters": {
        "url": "={{ $json.baseUrl }}/security/public-key-certificates",
        "options": {}
      },
      "typeVersion": 4.4
    },
    {
      "id": "n3b",
      "name": "Get Challenge",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        672,
        0
      ],
      "parameters": {
        "url": "={{ $('\u2699\ufe0f Config').first().json.baseUrl }}/auth/challenge",
        "method": "POST",
        "options": {}
      },
      "typeVersion": 4.4
    },
    {
      "id": "n4",
      "name": "Encrypt Token",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        0
      ],
      "parameters": {
        "jsCode": "// RSA-OAEP SHA-256 encryption of KSeF token\n// Format: authToken|timestampMs encrypted with KSeF public certificate\nconst crypto = require('crypto');\nconst config = $('\u2699\ufe0f Config').first().json;\n\nconst challengeData = $input.first().json;\nconst challenge = challengeData.challenge;\nconst timestampMs = challengeData.timestampMs;\n\n// Get public key(s) from Get Public Key node\nconst keyItems = $('Get Public Key').all();\nlet publicKeys;\nif (Array.isArray(keyItems[0].json)) {\n  publicKeys = keyItems[0].json;\n} else if (keyItems.length > 1) {\n  publicKeys = keyItems.map(i => i.json);\n} else {\n  publicKeys = [keyItems[0].json];\n}\n\n// Find the key for token encryption\nconst encKey = publicKeys.find(k => \n  (k.usage && k.usage.includes('KsefTokenEncryption')) ||\n  (k.usageTypes && k.usageTypes.includes('KsefTokenEncryption'))\n) || publicKeys[0];\n\n// Prepare plaintext: authToken|timestampMs\nconst plaintext = `${config.authToken}|${timestampMs}`;\n\n// Convert certificate to PEM format with proper 64-char line breaks\nconst certBase64 = typeof encKey === 'string' ? encKey : (encKey.certificate || encKey.publicKey);\nlet certPem;\nif (certBase64.includes('-----BEGIN')) {\n  certPem = certBase64;\n} else {\n  const formatted = certBase64.match(/.{1,64}/g).join('\\n');\n  certPem = `-----BEGIN CERTIFICATE-----\\n${formatted}\\n-----END CERTIFICATE-----`;\n}\n\n// Encrypt with RSA-OAEP SHA-256 + MGF1\nconst encryptedBuffer = crypto.publicEncrypt(\n  {\n    key: certPem,\n    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,\n    oaepHash: 'sha256'\n  },\n  Buffer.from(plaintext)\n);\nconst encryptedToken = encryptedBuffer.toString('base64');\n\nreturn [{\n  json: {\n    challenge: challenge,\n    contextIdentifier: {\n      type: 'Nip',\n      value: config.nip\n    },\n    encryptedToken: encryptedToken\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "n5",
      "name": "Init Auth",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1104,
        0
      ],
      "parameters": {
        "url": "={{ $('\u2699\ufe0f Config').first().json.baseUrl }}/auth/ksef-token",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "n5a",
      "name": "Wait 2s",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        0
      ],
      "parameters": {
        "jsCode": "// KSeF needs a moment to process authentication\n// 2 seconds is usually enough\nawait new Promise(r => setTimeout(r, 2000));\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "n5b",
      "name": "Check Auth Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1552,
        0
      ],
      "parameters": {
        "url": "={{ $('\u2699\ufe0f Config').first().json.baseUrl }}/auth/{{ $('Init Auth').first().json.referenceNumber }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Init Auth').first().json.authenticationToken.token }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "n5c",
      "name": "Redeem Token",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1760,
        0
      ],
      "parameters": {
        "url": "={{ $('\u2699\ufe0f Config').first().json.baseUrl }}/auth/token/redeem",
        "method": "POST",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Init Auth').first().json.authenticationToken.token }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "n6",
      "name": "Query Invoices",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1984,
        0
      ],
      "parameters": {
        "url": "={{ $('\u2699\ufe0f Config').first().json.baseUrl }}/invoices/query/metadata",
        "method": "POST",
        "options": {
          "pagination": {
            "pagination": {
              "parameters": {
                "parameters": [
                  {
                    "name": "pageOffset",
                    "value": "={{ $pageCount }}"
                  }
                ]
              },
              "completeExpression": "={{ $response.body.hasMore === false }}",
              "paginationCompleteWhen": "other"
            }
          }
        },
        "jsonBody": "={{ JSON.stringify({ subjectType: $('\u2699\ufe0f Config').first().json.subjectType, dateRange: { dateType: 'PermanentStorage', from: $('\u2699\ufe0f Config').first().json.startDate, to: $('\u2699\ufe0f Config').first().json.endDate } }) }}",
        "sendBody": true,
        "sendQuery": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "queryParameters": {
          "parameters": [
            {
              "name": "pageSize",
              "value": "250"
            },
            {
              "name": "sortOrder",
              "value": "Desc"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Redeem Token').first().json.accessToken.token }}"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "n7",
      "name": "Extract Invoices",
      "type": "n8n-nodes-base.code",
      "position": [
        2208,
        0
      ],
      "parameters": {
        "jsCode": "// Collect all invoices from paginated results\nconst pages = $input.all();\nconst allInvoices = [];\n\nfor (const page of pages) {\n  if (page.json.invoices && Array.isArray(page.json.invoices)) {\n    allInvoices.push(...page.json.invoices);\n  }\n}\n\nif (allInvoices.length === 0) {\n  return [{ json: { _noInvoices: true, message: 'No invoices found in the specified date range', count: 0 } }];\n}\n\nconsole.log(`Found ${allInvoices.length} invoices`);\nreturn allInvoices.map(inv => ({ json: inv }));"
      },
      "typeVersion": 2
    },
    {
      "id": "n10",
      "name": "Format for Spreadsheet",
      "type": "n8n-nodes-base.code",
      "position": [
        2432,
        0
      ],
      "parameters": {
        "jsCode": "// Flatten nested KSeF metadata into clean spreadsheet columns\nconst items = $input.all();\n\nif (items.length === 1 && items[0].json._noInvoices) {\n  return [{ json: { message: 'No invoices to export' } }];\n}\n\nconst rows = items.map(item => {\n  const inv = item.json;\n  return {\n    json: {\n      'KSeF Number': inv.ksefNumber || '',\n      'Invoice Number': inv.invoiceNumber || '',\n      'Issue Date': inv.issueDate || '',\n      'Invoicing Date': inv.invoicingDate ? inv.invoicingDate.split('T')[0] : '',\n      'Acquisition Date': inv.acquisitionDate ? inv.acquisitionDate.split('T')[0] : '',\n      'Seller NIP': inv.seller?.nip || '',\n      'Seller Name': inv.seller?.name || '',\n      'Buyer NIP': inv.buyer?.identifier?.value || '',\n      'Buyer Name': inv.buyer?.name || '',\n      'Net Amount': inv.netAmount || 0,\n      'VAT Amount': inv.vatAmount || 0,\n      'Gross Amount': inv.grossAmount || 0,\n      'Currency': inv.currency || 'PLN',\n      'Invoice Type': inv.invoiceType || '',\n      'Has Attachment': inv.hasAttachment ? 'Yes' : 'No',\n      'Self Invoicing': inv.isSelfInvoicing ? 'Yes' : 'No'\n    }\n  };\n});\n\nconsole.log(`Formatted ${rows.length} rows for spreadsheet`);\nreturn rows;"
      },
      "typeVersion": 2
    },
    {
      "id": "n11",
      "name": "Write XLSX",
      "type": "n8n-nodes-base.spreadsheetFile",
      "position": [
        2640,
        0
      ],
      "parameters": {
        "options": {
          "fileName": "ksef_invoices.xlsx",
          "headerRow": true,
          "sheetName": "Invoices"
        },
        "operation": "toFile",
        "fileFormat": "xlsx"
      },
      "typeVersion": 2
    },
    {
      "id": "n9",
      "name": "Close Session",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        2864,
        0
      ],
      "parameters": {
        "url": "={{ $('\u2699\ufe0f Config').first().json.baseUrl }}/auth/sessions/current",
        "method": "DELETE",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Redeem Token').first().json.accessToken.token }}"
            }
          ]
        }
      },
      "typeVersion": 4.4
    }
  ],
  "active": false,
  "settings": {
    "timezone": "Europe/Warsaw",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "fc53a640-2d3b-49af-a9a9-c41be97af326",
  "connections": {
    "Wait 2s": {
      "main": [
        [
          {
            "node": "Check Auth Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Init Auth": {
      "main": [
        [
          {
            "node": "Wait 2s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write XLSX": {
      "main": [
        [
          {
            "node": "Close Session",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Redeem Token": {
      "main": [
        [
          {
            "node": "Query Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Encrypt Token": {
      "main": [
        [
          {
            "node": "Init Auth",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Challenge": {
      "main": [
        [
          {
            "node": "Encrypt Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2699\ufe0f Config": {
      "main": [
        [
          {
            "node": "Get Public Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Public Key": {
      "main": [
        [
          {
            "node": "Get Challenge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Invoices": {
      "main": [
        [
          {
            "node": "Extract Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Invoices": {
      "main": [
        [
          {
            "node": "Format for Spreadsheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Auth Status": {
      "main": [
        [
          {
            "node": "Redeem Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format for Spreadsheet": {
      "main": [
        [
          {
            "node": "Write XLSX",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking 'Test workflow'": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Download invoices from Poland's KSeF (Krajowy System e-Faktur) and export them as an XLSX spreadsheet. Handles the full v2 authentication flow automatically.

Source: https://n8n.io/workflows/13925/ — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

Working With Excel Spreadsheet Files Xls Xlsx. Uses stickyNote, readBinaryFile, manualTrigger, writeBinaryFile. Event-driven trigger; 24 nodes.

Read Binary File, Write Binary File, Google Drive +4
Data & Sheets

This workflow will help guide you through obtaining a spreadsheet file, reading it, making a change then saving it to local or cloud storage.

Read Binary File, Write Binary File, Google Drive +4
Data & Sheets

Monitor Azure subscription resources with cost and usage tracking

HTTP Request, Spreadsheet File
Data & Sheets

Upload Bulk Records From Csv Airtable Interfaces. Uses airtable, httpRequest, stickyNote, spreadsheetFile. Event-driven trigger; 17 nodes.

Airtable, HTTP Request, Spreadsheet File +1
Data & Sheets

This workflow is a supporting automation to a common Airtable situation, that as of this writing, has no direct solution but has great demand.

Airtable, HTTP Request, Spreadsheet File +1