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.
cdp_router. Uses gmailTrigger, telegramTrigger, googleSheets, httpRequest. Event-driven trigger; 53 nodes.
cdp_router. Uses gmailTrigger, telegramTrigger, googleSheets, httpRequest. Event-driven trigger; 53 nodes.
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.
Workflow 1826. Uses readBinaryFile, writeBinaryFile, googleDrive, microsoftOneDrive. Event-driven trigger; 24 nodes.