diff --git a/workflows/Download KSeF (Poland’s e-invoicing system) invoices to an Excel spreadsheet-13925/readme-13925.md b/workflows/Download KSeF (Poland’s e-invoicing system) invoices to an Excel spreadsheet-13925/readme-13925.md
new file mode 100644
index 000000000..2689ce067
--- /dev/null
+++ b/workflows/Download KSeF (Poland’s e-invoicing system) invoices to an Excel spreadsheet-13925/readme-13925.md
@@ -0,0 +1,668 @@
+Download KSeF (Poland’s e-invoicing system) invoices to an Excel spreadsheet
+
+https://n8nworkflows.xyz/workflows/download-ksef--poland-s-e-invoicing-system--invoices-to-an-excel-spreadsheet-13925
+
+
+# Download KSeF (Poland’s e-invoicing system) invoices to an Excel spreadsheet
+
+# 1. Workflow Overview
+
+This workflow authenticates against Poland’s **KSeF v2 API**, retrieves invoice metadata for a specified date range, transforms the returned data into flat tabular rows, and exports the result as an **XLSX spreadsheet**.
+
+Typical use cases:
+- Downloading invoice metadata you **received** or **issued**
+- Building a spreadsheet export for accounting or reporting
+- Using KSeF data as a starting point for email, storage, database, or spreadsheet integrations
+
+The workflow is organized into four logical blocks:
+
+## 1.1 Manual Start and Configuration
+The workflow starts manually and defines all user-editable runtime settings in a single configuration node: API base URL, NIP, KSeF token, date range, and invoice subject type.
+
+## 1.2 KSeF Authentication Flow
+KSeF uses a multi-step authentication model. The workflow retrieves the public certificate, requests a challenge, encrypts the token with RSA-OAEP SHA-256, initializes authentication, waits briefly, checks status, and redeems the temporary token for an access token.
+
+## 1.3 Invoice Query and Pagination
+Using the final access token, the workflow queries invoice metadata from KSeF with pagination enabled until all pages are retrieved.
+
+## 1.4 Data Flattening, Spreadsheet Export, and Session Cleanup
+The returned invoice metadata is normalized into spreadsheet-friendly columns, written to an XLSX file, and the API session is then closed.
+
+---
+
+# 2. Block-by-Block Analysis
+
+## 2.1 Manual Start and Configuration
+
+### Overview
+This block provides the entry point and all required runtime inputs. It centralizes user configuration so the rest of the workflow can reference these values via expressions.
+
+### Nodes Involved
+- `When clicking 'Test workflow'`
+- `⚙️ Config`
+
+### Node Details
+
+#### When clicking 'Test workflow'
+- **Type and technical role:** `n8n-nodes-base.manualTrigger`
+ Manual execution trigger for testing or ad hoc runs.
+- **Configuration choices:** No special configuration.
+- **Key expressions or variables used:** None.
+- **Input and output connections:**
+ - Input: none
+ - Output: `⚙️ Config`
+- **Version-specific requirements:** Type version `1`.
+- **Edge cases or potential failure types:** None; only runs when manually triggered.
+- **Sub-workflow reference:** None.
+
+#### ⚙️ Config
+- **Type and technical role:** `n8n-nodes-base.set`
+ Defines workflow parameters as raw JSON.
+- **Configuration choices:** Uses **raw mode** and outputs a single JSON object containing:
+ - `baseUrl`: `https://api.ksef.mf.gov.pl/v2`
+ - `nip`: user NIP
+ - `authToken`: KSeF authorization token
+ - `startDate`: ISO 8601 datetime
+ - `endDate`: ISO 8601 datetime
+ - `subjectType`: `Subject1` or `Subject2`
+- **Key expressions or variables used:** These fields are later referenced with expressions such as:
+ - `$('⚙️ Config').first().json.baseUrl`
+ - `$('⚙️ Config').first().json.nip`
+ - `$('⚙️ Config').first().json.authToken`
+- **Input and output connections:**
+ - Input: `When clicking 'Test workflow'`
+ - Output: `Get Public Key`
+- **Version-specific requirements:** Type version `3.4`.
+- **Edge cases or potential failure types:**
+ - Placeholder values left unchanged
+ - Invalid NIP format
+ - Invalid or expired KSeF token
+ - Incorrect date format
+ - `subjectType` not accepted by KSeF
+- **Sub-workflow reference:** None.
+
+---
+
+## 2.2 KSeF Authentication Flow
+
+### Overview
+This block performs the full KSeF v2 authentication sequence. It converts the long-lived KSeF authorization token into a temporary auth token, waits for processing, then redeems it into the usable API access token.
+
+### Nodes Involved
+- `Get Public Key`
+- `Get Challenge`
+- `Encrypt Token`
+- `Init Auth`
+- `Wait 2s`
+- `Check Auth Status`
+- `Redeem Token`
+
+### Node Details
+
+#### Get Public Key
+- **Type and technical role:** `n8n-nodes-base.httpRequest`
+ Fetches KSeF public certificate data used for token encryption.
+- **Configuration choices:** Sends a GET request to:
+ - `{{$json.baseUrl}}/security/public-key-certificates`
+- **Key expressions or variables used:**
+ - `={{ $json.baseUrl }}/security/public-key-certificates`
+- **Input and output connections:**
+ - Input: `⚙️ Config`
+ - Output: `Get Challenge`
+- **Version-specific requirements:** Type version `4.4`.
+- **Edge cases or potential failure types:**
+ - KSeF endpoint unavailable
+ - SSL/network issues
+ - Unexpected response structure
+- **Sub-workflow reference:** None.
+
+#### Get Challenge
+- **Type and technical role:** `n8n-nodes-base.httpRequest`
+ Requests a challenge and timestamp from KSeF.
+- **Configuration choices:** POST request to:
+ - `{{ $('⚙️ Config').first().json.baseUrl }}/auth/challenge`
+- **Key expressions or variables used:**
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/challenge`
+- **Input and output connections:**
+ - Input: `Get Public Key`
+ - Output: `Encrypt Token`
+- **Version-specific requirements:** Type version `4.4`.
+- **Edge cases or potential failure types:**
+ - Endpoint unavailable
+ - API contract changes
+ - Missing `timestampMs` or `challenge` in response
+- **Sub-workflow reference:** None.
+
+#### Encrypt Token
+- **Type and technical role:** `n8n-nodes-base.code`
+ Executes Node.js code to build and encrypt the `authToken|timestampMs` payload with the KSeF public certificate.
+- **Configuration choices:**
+ - Imports `crypto`
+ - Reads config from `⚙️ Config`
+ - Reads challenge data from current input
+ - Reads public keys from `Get Public Key`
+ - Selects a certificate intended for `KsefTokenEncryption`
+ - Converts a certificate string to PEM if needed
+ - Uses RSA OAEP padding with SHA-256
+ - Outputs:
+ - `challenge`
+ - `contextIdentifier.type = "Nip"`
+ - `contextIdentifier.value = config.nip`
+ - `encryptedToken`
+- **Key expressions or variables used:**
+ - `$('⚙️ Config').first().json`
+ - `$('Get Public Key').all()`
+ - `$input.first().json`
+- **Input and output connections:**
+ - Input: `Get Challenge`
+ - Output: `Init Auth`
+- **Version-specific requirements:** Type version `2`.
+ Requires code node execution with access to Node.js built-in `crypto`.
+- **Edge cases or potential failure types:**
+ - Missing or malformed certificate
+ - Certificate array shape differs from expected logic
+ - `match(/.{1,64}/g)` could fail if certificate string is empty
+ - `authToken` or `timestampMs` missing
+ - Runtime restrictions in environments that limit `require('crypto')`
+- **Sub-workflow reference:** None.
+
+#### Init Auth
+- **Type and technical role:** `n8n-nodes-base.httpRequest`
+ Sends the encrypted payload to initialize authentication.
+- **Configuration choices:**
+ - POST to `/auth/ksef-token`
+ - Sends JSON body from current item
+ - Explicit `Content-Type: application/json`
+- **Key expressions or variables used:**
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/ksef-token`
+ - `={{ JSON.stringify($json) }}`
+- **Input and output connections:**
+ - Input: `Encrypt Token`
+ - Output: `Wait 2s`
+- **Version-specific requirements:** Type version `4.4`.
+- **Edge cases or potential failure types:**
+ - Invalid encrypted payload
+ - Invalid NIP/token binding
+ - HTTP 4xx/5xx from KSeF
+ - Temporary `authenticationToken` returned but auth not yet ready
+- **Sub-workflow reference:** None.
+
+#### Wait 2s
+- **Type and technical role:** `n8n-nodes-base.code`
+ Introduces a fixed delay before checking auth status.
+- **Configuration choices:** Uses `setTimeout` for 2000 ms, then returns input unchanged.
+- **Key expressions or variables used:** `$input.all()`
+- **Input and output connections:**
+ - Input: `Init Auth`
+ - Output: `Check Auth Status`
+- **Version-specific requirements:** Type version `2`.
+- **Edge cases or potential failure types:**
+ - Two seconds may be insufficient under KSeF latency or load
+ - No retry loop exists if status is not ready yet
+- **Sub-workflow reference:** None.
+
+#### Check Auth Status
+- **Type and technical role:** `n8n-nodes-base.httpRequest`
+ Polls the auth session status using the temporary authentication token.
+- **Configuration choices:**
+ - GET to `/auth/{referenceNumber}`
+ - Authorization header uses temp token from `Init Auth`
+ - Accept header set to `application/json`
+- **Key expressions or variables used:**
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/{{ $('Init Auth').first().json.referenceNumber }}`
+ - `=Bearer {{ $('Init Auth').first().json.authenticationToken.token }}`
+- **Input and output connections:**
+ - Input: `Wait 2s`
+ - Output: `Redeem Token`
+- **Version-specific requirements:** Type version `4.4`.
+- **Edge cases or potential failure types:**
+ - Status still pending after 2 seconds
+ - Missing `referenceNumber`
+ - Temporary token expired
+ - Endpoint errors not explicitly handled
+- **Sub-workflow reference:** None.
+
+#### Redeem Token
+- **Type and technical role:** `n8n-nodes-base.httpRequest`
+ Exchanges the temporary auth token for the final access token.
+- **Configuration choices:**
+ - POST to `/auth/token/redeem`
+ - Authorization uses temp token from `Init Auth`
+ - Accept header set to `application/json`
+- **Key expressions or variables used:**
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/token/redeem`
+ - `=Bearer {{ $('Init Auth').first().json.authenticationToken.token }}`
+- **Input and output connections:**
+ - Input: `Check Auth Status`
+ - Output: `Query Invoices`
+- **Version-specific requirements:** Type version `4.4`.
+- **Edge cases or potential failure types:**
+ - Attempting redeem before auth is actually ready
+ - Expired temporary token
+ - KSeF may reject flow if previous status was unsuccessful
+- **Sub-workflow reference:** None.
+
+---
+
+## 2.3 Invoice Query and Pagination
+
+### Overview
+This block requests invoice metadata from KSeF using the access token and automatically paginates through all available result pages.
+
+### Nodes Involved
+- `Query Invoices`
+- `Extract Invoices`
+
+### Node Details
+
+#### Query Invoices
+- **Type and technical role:** `n8n-nodes-base.httpRequest`
+ Queries KSeF invoice metadata with body filters and query pagination controls.
+- **Configuration choices:**
+ - POST to `/invoices/query/metadata`
+ - Sends JSON body:
+ - `subjectType` from config
+ - `dateRange.dateType = "PermanentStorage"`
+ - `from` and `to` from config
+ - Query parameters:
+ - `pageSize = 250`
+ - `sortOrder = Desc`
+ - Authorization uses redeemed access token
+ - Pagination:
+ - `pageOffset = {{$pageCount}}`
+ - stops when `{{$response.body.hasMore === false}}`
+- **Key expressions or variables used:**
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/invoices/query/metadata`
+ - `={{ JSON.stringify({ subjectType: $('⚙️ Config').first().json.subjectType, dateRange: { dateType: 'PermanentStorage', from: $('⚙️ Config').first().json.startDate, to: $('⚙️ Config').first().json.endDate } }) }}`
+ - `=Bearer {{ $('Redeem Token').first().json.accessToken.token }}`
+ - `={{ $pageCount }}`
+ - `={{ $response.body.hasMore === false }}`
+- **Input and output connections:**
+ - Input: `Redeem Token`
+ - Output: `Extract Invoices`
+- **Version-specific requirements:** Type version `4.4`.
+ Uses built-in HTTP Request pagination features.
+- **Edge cases or potential failure types:**
+ - Access token expired or invalid
+ - API response lacks `hasMore`
+ - `pageOffset` semantics may differ if KSeF changes paging behavior
+ - Large result sets could increase runtime
+ - Date range may return no data
+- **Sub-workflow reference:** None.
+
+#### Extract Invoices
+- **Type and technical role:** `n8n-nodes-base.code`
+ Aggregates invoice arrays from all paginated HTTP responses into a single item stream.
+- **Configuration choices:**
+ - Iterates over all pages
+ - Collects `page.json.invoices`
+ - If none found, emits a single marker item:
+ - `_noInvoices: true`
+ - message
+ - count: 0
+ - Otherwise emits one item per invoice
+- **Key expressions or variables used:** `$input.all()`
+- **Input and output connections:**
+ - Input: `Query Invoices`
+ - Output: `Format for Spreadsheet`
+- **Version-specific requirements:** Type version `2`.
+- **Edge cases or potential failure types:**
+ - Response structure may not contain `invoices`
+ - Empty result set intentionally produces a special record instead of failing
+ - Very large datasets may increase memory usage because all pages are collected in memory
+- **Sub-workflow reference:** None.
+
+---
+
+## 2.4 Data Flattening, Spreadsheet Export, and Session Cleanup
+
+### Overview
+This block converts nested KSeF invoice metadata into flat spreadsheet columns, creates an XLSX binary file, and then closes the active KSeF session.
+
+### Nodes Involved
+- `Format for Spreadsheet`
+- `Write XLSX`
+- `Close Session`
+
+### Node Details
+
+#### Format for Spreadsheet
+- **Type and technical role:** `n8n-nodes-base.code`
+ Maps invoice objects into spreadsheet-ready rows with fixed column names.
+- **Configuration choices:**
+ - If `_noInvoices` marker is present, returns one row with `message: 'No invoices to export'`
+ - Otherwise maps each invoice into columns such as:
+ - `KSeF Number`
+ - `Invoice Number`
+ - `Issue Date`
+ - `Invoicing Date`
+ - `Acquisition Date`
+ - `Seller NIP`
+ - `Seller Name`
+ - `Buyer NIP`
+ - `Buyer Name`
+ - `Net Amount`
+ - `VAT Amount`
+ - `Gross Amount`
+ - `Currency`
+ - `Invoice Type`
+ - `Has Attachment`
+ - `Self Invoicing`
+ - Uses optional chaining and fallback values for missing fields
+ - Splits dates at `T` for some date fields
+- **Key expressions or variables used:** `$input.all()`
+- **Input and output connections:**
+ - Input: `Extract Invoices`
+ - Output: `Write XLSX`
+- **Version-specific requirements:** Type version `2`.
+- **Edge cases or potential failure types:**
+ - If invoice schema changes, some columns may be blank
+ - Buyer identifier path assumes `buyer.identifier.value`
+ - Numeric defaults of `0` may blur distinction between missing and actual zero values
+ - Empty output still creates spreadsheet content if `_noInvoices` case occurs
+- **Sub-workflow reference:** None.
+
+#### Write XLSX
+- **Type and technical role:** `n8n-nodes-base.spreadsheetFile`
+ Converts JSON rows into an XLSX file in binary output.
+- **Configuration choices:**
+ - Operation: `toFile`
+ - File format: `xlsx`
+ - File name: `ksef_invoices.xlsx`
+ - Sheet name: `Invoices`
+ - Header row enabled
+- **Key expressions or variables used:** None.
+- **Input and output connections:**
+ - Input: `Format for Spreadsheet`
+ - Output: `Close Session`
+- **Version-specific requirements:** Type version `2`.
+- **Edge cases or potential failure types:**
+ - Large datasets may create large binary files
+ - If upstream returns only a message row, the file still gets created
+ - Binary output must be handled explicitly if saving externally
+- **Sub-workflow reference:** None.
+
+#### Close Session
+- **Type and technical role:** `n8n-nodes-base.httpRequest`
+ Deletes the current KSeF auth session after export.
+- **Configuration choices:**
+ - DELETE to `/auth/sessions/current`
+ - Authorization uses final access token
+ - `onError` is set to `continueRegularOutput`
+- **Key expressions or variables used:**
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/sessions/current`
+ - `=Bearer {{ $('Redeem Token').first().json.accessToken.token }}`
+- **Input and output connections:**
+ - Input: `Write XLSX`
+ - Output: none
+- **Version-specific requirements:** Type version `4.4`.
+- **Edge cases or potential failure types:**
+ - Session may already be expired or closed
+ - Cleanup failure does not stop normal workflow output because error handling continues
+- **Sub-workflow reference:** None.
+
+---
+
+# 3. Summary Table
+
+| Node Name | Node Type | Functional Role | Input Node(s) | Output Node(s) | Sticky Note |
+|---|---|---|---|---|---|
+| When clicking 'Test workflow' | Manual Trigger | Manual workflow start | | ⚙️ Config | |
+| ⚙️ Config | Set | Central runtime configuration for KSeF API, credentials, date range, and subject type | When clicking 'Test workflow' | Get Public Key | ## 🔧 Configuration
Edit the JSON below to set your:
- **nip** — your 10-digit NIP number
- **authToken** — KSeF authorization token
- **startDate / endDate** — ISO 8601 format
- **subjectType** —
`Subject2` = invoices you **received** (buyer)
`Subject1` = invoices you **issued** (seller)
Dates use `PermanentStorage` type (when KSeF stored the invoice). |
+| Get Public Key | HTTP Request | Fetch KSeF public encryption certificate(s) | ⚙️ Config | Get Challenge | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Get Challenge | HTTP Request | Request KSeF challenge and timestamp | Get Public Key | Encrypt Token | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Encrypt Token | Code | Build and RSA-encrypt KSeF token payload | Get Challenge | Init Auth | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Init Auth | HTTP Request | Initialize KSeF authentication with encrypted token | Encrypt Token | Wait 2s | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Wait 2s | Code | Delay before auth status polling | Init Auth | Check Auth Status | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Check Auth Status | HTTP Request | Poll auth status using temporary token | Wait 2s | Redeem Token | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Redeem Token | HTTP Request | Exchange temporary auth token for final access token | Check Auth Status | Query Invoices | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Query Invoices | HTTP Request | Query paginated invoice metadata | Redeem Token | Extract Invoices | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Extract Invoices | Code | Merge paginated invoice arrays into item stream | Query Invoices | Format for Spreadsheet | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Format for Spreadsheet | Code | Flatten invoice metadata into spreadsheet columns | Extract Invoices | Write XLSX | ## 📊 Output
Invoice metadata is flattened into columns:
KSeF Number, Invoice Number, Issue Date, Seller/Buyer NIP & Name, Net/VAT/Gross amounts, Currency, Type.
The XLSX file is available as binary output in the **Write XLSX** node.
To save to disk, connect a **Write Binary File** node after Write XLSX.
To email it, connect a **Send Email** node and attach the binary.
Swap it to Google Spreadsheet or database of your choice. |
+| Write XLSX | Spreadsheet File | Generate XLSX file from flattened rows | Format for Spreadsheet | Close Session | ## 📊 Output
Invoice metadata is flattened into columns:
KSeF Number, Invoice Number, Issue Date, Seller/Buyer NIP & Name, Net/VAT/Gross amounts, Currency, Type.
The XLSX file is available as binary output in the **Write XLSX** node.
To save to disk, connect a **Write Binary File** node after Write XLSX.
To email it, connect a **Send Email** node and attach the binary.
Swap it to Google Spreadsheet or database of your choice. |
+| Close Session | HTTP Request | Close current KSeF session after export | Write XLSX | | ## 📊 Output
Invoice metadata is flattened into columns:
KSeF Number, Invoice Number, Issue Date, Seller/Buyer NIP & Name, Net/VAT/Gross amounts, Currency, Type.
The XLSX file is available as binary output in the **Write XLSX** node.
To save to disk, connect a **Write Binary File** node after Write XLSX.
To email it, connect a **Send Email** node and attach the binary.
Swap it to Google Spreadsheet or database of your choice. |
+| Sticky Note | Sticky Note | Visual documentation and quick start guidance | | | ## 🇵🇱 KSeF — Download Invoices to Spreadsheet
Downloads invoice metadata from Poland's **KSeF** (Krajowy System e-Faktur) and exports it as an **XLSX spreadsheet**.
### Quick Start
1. Open the **⚙️ Config** node and fill in your **NIP** and **KSeF token**
2. Set the **date range** (startDate / endDate)
3. Click **Test workflow**
4. Your spreadsheet will appear in the **Write XLSX** node output
### How to get a KSeF token
Generate an authorization token at [ksef.mf.gov.pl](https://ksef.mf.gov.pl) → Log in → Manage tokens.
Tokens look like: `YYYYMMDD-XX-XXXXXXXXXX-XXXXXXXXXX-XX\|nip-XXXXXXXXXX\|hash`
### Need help?
KSeF docs: https://www.gov.pl/web/kas/krajowy-system-e-faktur
Made with ❤️ by [Greg Brzezinka](greg@prosit.no) Need help? [Reach out to me](https://www.linkedin.com/in/brzezinka)! |
+| Sticky Note1 | Sticky Note | Visual configuration instructions | | | ## 🔧 Configuration
Edit the JSON below to set your:
- **nip** — your 10-digit NIP number
- **authToken** — KSeF authorization token
- **startDate / endDate** — ISO 8601 format
- **subjectType** —
`Subject2` = invoices you **received** (buyer)
`Subject1` = invoices you **issued** (seller)
Dates use `PermanentStorage` type (when KSeF stored the invoice). |
+| Sticky Note2 | Sticky Note | Visual explanation of KSeF auth sequence | | | ## 🔐 Authentication Flow (v2 API)
KSeF uses a multi-step auth:
1. **Get Public Key** — fetch RSA certificate
2. **Get Challenge** — get a challenge + timestamp
3. **Encrypt Token** — RSA-OAEP encrypt `token\|timestamp`
4. **Init Auth** — submit encrypted token, get temp JWT
5. **Wait + Check Status** — poll until auth is ready
6. **Redeem Token** — exchange temp JWT for access token
⚠️ The `authenticationToken` from step 4 is **temporary**!
Only the `accessToken` from step 6 works for API calls. |
+| Sticky Note3 | Sticky Note | Visual description of spreadsheet output | | | ## 📊 Output
Invoice metadata is flattened into columns:
KSeF Number, Invoice Number, Issue Date, Seller/Buyer NIP & Name, Net/VAT/Gross amounts, Currency, Type.
The XLSX file is available as binary output in the **Write XLSX** node.
To save to disk, connect a **Write Binary File** node after Write XLSX.
To email it, connect a **Send Email** node and attach the binary.
Swap it to Google Spreadsheet or database of your choice. |
+
+---
+
+# 4. Reproducing the Workflow from Scratch
+
+1. **Create a new workflow**
+ Name it something like: `KSeF - Download Invoices to Spreadsheet`.
+
+2. **Add a Manual Trigger node**
+ - Node type: **Manual Trigger**
+ - Keep default settings.
+ - This is the workflow entry point.
+
+3. **Add a Set node named `⚙️ Config`**
+ - Node type: **Set**
+ - Set mode to **Raw**
+ - Paste a JSON object with these keys:
+ - `baseUrl`
+ - `nip`
+ - `authToken`
+ - `startDate`
+ - `endDate`
+ - `subjectType`
+ - Example values:
+ - `baseUrl`: `https://api.ksef.mf.gov.pl/v2`
+ - `nip`: your 10-digit NIP
+ - `authToken`: your KSeF token
+ - `startDate`: ISO timestamp like `2026-02-01T00:00:00Z`
+ - `endDate`: ISO timestamp like `2026-03-06T23:59:59Z`
+ - `subjectType`: `Subject2` for received invoices or `Subject1` for issued invoices
+ - Connect **Manual Trigger → ⚙️ Config**
+
+4. **Add an HTTP Request node named `Get Public Key`**
+ - Method: **GET**
+ - URL:
+ - `={{ $json.baseUrl }}/security/public-key-certificates`
+ - No authentication required.
+ - Connect **⚙️ Config → Get Public Key**
+
+5. **Add an HTTP Request node named `Get Challenge`**
+ - Method: **POST**
+ - URL:
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/challenge`
+ - No request body needed.
+ - Connect **Get Public Key → Get Challenge**
+
+6. **Add a Code node named `Encrypt Token`**
+ - Node type: **Code**
+ - Language: JavaScript
+ - Paste code that:
+ - imports Node’s `crypto`
+ - reads config from `$('⚙️ Config').first().json`
+ - reads `challenge` and `timestampMs` from the input item
+ - reads the public certificate(s) from `$('Get Public Key').all()`
+ - selects the key whose usage includes `KsefTokenEncryption` if present
+ - converts the certificate into PEM format if needed
+ - encrypts the plaintext `authToken|timestampMs` using:
+ - `RSA_PKCS1_OAEP_PADDING`
+ - `oaepHash: 'sha256'`
+ - returns JSON with:
+ - `challenge`
+ - `contextIdentifier: { type: 'Nip', value: config.nip }`
+ - `encryptedToken`
+ - Connect **Get Challenge → Encrypt Token**
+
+7. **Add an HTTP Request node named `Init Auth`**
+ - Method: **POST**
+ - URL:
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/ksef-token`
+ - Send Headers: enabled
+ - Header:
+ - `Content-Type: application/json`
+ - Send Body: enabled
+ - Body Content Type: **JSON**
+ - JSON body:
+ - `={{ JSON.stringify($json) }}`
+ - Connect **Encrypt Token → Init Auth**
+
+8. **Add a Code node named `Wait 2s`**
+ - Paste JavaScript:
+ - wait 2000 ms using `await new Promise(r => setTimeout(r, 2000));`
+ - return `$input.all();`
+ - Purpose: give KSeF time to prepare auth status.
+ - Connect **Init Auth → Wait 2s**
+
+9. **Add an HTTP Request node named `Check Auth Status`**
+ - Method: **GET**
+ - URL:
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/{{ $('Init Auth').first().json.referenceNumber }}`
+ - Send Headers: enabled
+ - Headers:
+ - `Authorization: =Bearer {{ $('Init Auth').first().json.authenticationToken.token }}`
+ - `Accept: application/json`
+ - Connect **Wait 2s → Check Auth Status**
+
+10. **Add an HTTP Request node named `Redeem Token`**
+ - Method: **POST**
+ - URL:
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/token/redeem`
+ - Send Headers: enabled
+ - Headers:
+ - `Authorization: =Bearer {{ $('Init Auth').first().json.authenticationToken.token }}`
+ - `Accept: application/json`
+ - No body required.
+ - Connect **Check Auth Status → Redeem Token**
+
+11. **Add an HTTP Request node named `Query Invoices`**
+ - Method: **POST**
+ - URL:
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/invoices/query/metadata`
+ - Send Headers: enabled
+ - Headers:
+ - `Content-Type: application/json`
+ - `Authorization: =Bearer {{ $('Redeem Token').first().json.accessToken.token }}`
+ - Send Query Parameters: enabled
+ - Query parameters:
+ - `pageSize = 250`
+ - `sortOrder = Desc`
+ - Send Body: enabled
+ - Body Content Type: **JSON**
+ - JSON body should contain:
+ - `subjectType` from config
+ - `dateRange.dateType = PermanentStorage`
+ - `dateRange.from = startDate`
+ - `dateRange.to = endDate`
+ - Use this expression:
+ - `={{ JSON.stringify({ subjectType: $('⚙️ Config').first().json.subjectType, dateRange: { dateType: 'PermanentStorage', from: $('⚙️ Config').first().json.startDate, to: $('⚙️ Config').first().json.endDate } }) }}`
+ - Enable **pagination**
+ - Configure pagination:
+ - parameter name: `pageOffset`
+ - value: `={{ $pageCount }}`
+ - completion expression: `={{ $response.body.hasMore === false }}`
+ - completion condition type: **other**
+ - Connect **Redeem Token → Query Invoices**
+
+12. **Add a Code node named `Extract Invoices`**
+ - JavaScript logic:
+ - read all pages using `$input.all()`
+ - collect each page’s `json.invoices` array
+ - concatenate all invoices into one list
+ - if no invoices found, return one item with:
+ - `_noInvoices: true`
+ - message
+ - count: 0
+ - otherwise return one item per invoice
+ - Connect **Query Invoices → Extract Invoices**
+
+13. **Add a Code node named `Format for Spreadsheet`**
+ - JavaScript logic:
+ - if input contains only `_noInvoices`, return one row with `message: 'No invoices to export'`
+ - otherwise map invoice data into flat columns:
+ - `KSeF Number`
+ - `Invoice Number`
+ - `Issue Date`
+ - `Invoicing Date`
+ - `Acquisition Date`
+ - `Seller NIP`
+ - `Seller Name`
+ - `Buyer NIP`
+ - `Buyer Name`
+ - `Net Amount`
+ - `VAT Amount`
+ - `Gross Amount`
+ - `Currency`
+ - `Invoice Type`
+ - `Has Attachment`
+ - `Self Invoicing`
+ - use optional chaining and default values
+ - for `invoicingDate` and `acquisitionDate`, split on `T` and keep the date part
+ - Connect **Extract Invoices → Format for Spreadsheet**
+
+14. **Add a Spreadsheet File node named `Write XLSX`**
+ - Operation: **To File**
+ - File format: **XLSX**
+ - File name: `ksef_invoices.xlsx`
+ - Sheet name: `Invoices`
+ - Header row: enabled
+ - This node will output a binary file.
+ - Connect **Format for Spreadsheet → Write XLSX**
+
+15. **Add an HTTP Request node named `Close Session`**
+ - Method: **DELETE**
+ - URL:
+ - `={{ $('⚙️ Config').first().json.baseUrl }}/auth/sessions/current`
+ - Send Headers: enabled
+ - Header:
+ - `Authorization: =Bearer {{ $('Redeem Token').first().json.accessToken.token }}`
+ - Set node error handling to **continue on error / continue regular output**
+ - This ensures session cleanup failure does not break the workflow.
+ - Connect **Write XLSX → Close Session**
+
+16. **Optional: add sticky notes**
+ - Add one note with quick-start information and KSeF links
+ - Add one near the config node describing `nip`, `authToken`, `startDate`, `endDate`, `subjectType`
+ - Add one near the auth chain explaining temp token vs access token
+ - Add one near the XLSX output explaining how to save or email the file
+
+17. **Set workflow settings**
+ - Timezone: `Europe/Warsaw`
+ - Execution order: `v1`
+
+18. **Test the workflow**
+ - Open `⚙️ Config`
+ - Replace placeholders:
+ - `YOUR_NIP_HERE`
+ - `YOUR_KSEF_TOKEN_HERE`
+ - Set the desired date range
+ - Choose `Subject1` or `Subject2`
+ - Click **Test workflow**
+
+19. **Validate outputs**
+ - `Redeem Token` should return an `accessToken`
+ - `Query Invoices` should return paginated results
+ - `Write XLSX` should expose binary output containing `ksef_invoices.xlsx`
+
+20. **Optional post-processing**
+ - To save locally: connect **Write Binary File** after `Write XLSX`
+ - To send by email: connect an email node and attach the binary
+ - To write elsewhere: replace or branch after `Format for Spreadsheet` or `Write XLSX`
+
+### Credential configuration
+This workflow does **not** use stored n8n credentials by default.
+Authentication is handled directly through the KSeF token placed in the `⚙️ Config` node and then converted into KSeF access tokens through API calls.
+
+### Sub-workflow setup
+There are **no sub-workflows** and no Execute Workflow nodes in this workflow.
+
+---
+
+# 5. General Notes & Resources
+
+| Note Content | Context or Link |
+|---|---|
+| Generate a KSeF authorization token from the KSeF portal under token management. | https://ksef.mf.gov.pl |
+| Official KSeF documentation and general information. | https://www.gov.pl/web/kas/krajowy-system-e-faktur |
+| Author credit: Greg Brzezinka | `greg@prosit.no` |
+| Contact / professional profile of the workflow author. | https://www.linkedin.com/in/brzezinka |
+| The workflow exports invoice metadata only, not the full invoice file payload. | General behavior |
+| The `authenticationToken` from `Init Auth` is temporary; API calls require the `accessToken` from `Redeem Token`. | Authentication design note |
+| The date filter uses `PermanentStorage` as the KSeF date type. | Query design note |
+| To persist the generated spreadsheet, attach a `Write Binary File`, email node, cloud storage node, or database flow after `Write XLSX`. | Extension pattern |
\ No newline at end of file