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 →
{
"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
}
]
]
}
}
}
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Working With Excel Spreadsheet Files Xls Xlsx. Uses stickyNote, readBinaryFile, manualTrigger, writeBinaryFile. Event-driven trigger; 24 nodes.
This workflow will help guide you through obtaining a spreadsheet file, reading it, making a change then saving it to local or cloud storage.
Monitor Azure subscription resources with cost and usage tracking
Upload Bulk Records From Csv Airtable Interfaces. Uses airtable, httpRequest, stickyNote, spreadsheetFile. Event-driven trigger; 17 nodes.
This workflow is a supporting automation to a common Airtable situation, that as of this writing, has no direct solution but has great demand.