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 →
{
"name": "Sellf: Purchase \u2192 KSeF Invoice",
"nodes": [
{
"parameters": {
"content": "## Create a KSeF invoice when a Sellf purchase completes\n\n### How it works\nThis workflow reacts to [Sellf](https://github.com/jurczykpawel/sellf) - a self-hosted checkout platform for digital products - which sends a signed `purchase.completed` webhook for every paid order. It verifies the `X-Sellf-Signature` header, checks that the customer asked for an invoice and supplied a NIP, builds a KSeF-ready payload, and sends it to your self-hosted [KSeF Gateway](https://github.com/jurczykpawel/ksef-gateway) - a small HTTP wrapper around Poland's official e-invoice API. On success the returned `ksefNumber` is captured; on failure the error is captured so you can alert on it.\n\n### Setup\n1. Deploy a KSeF Gateway instance (Docker, Render, or StackPilot - see the project README) and note its URL. New to KSeF, or need a certificate (KSeF tokens are being phased out)? See [the main README](https://github.com/jurczykpawel/ksef-gateway#certificate-based-auth-alternative-to-tokens).\n2. Open **Configuration (EDIT ME)** and fill in your gateway URL, gateway API key, seller details, and the webhook signing secret from Sellf \u2192 Settings \u2192 Webhooks.\n3. Copy the **Sellf Webhook** node's Production URL into that same Sellf settings page.\n4. See the red note on **Verify Signature** for one required n8n environment setting.\n\n### Customization tips\nConnect **Log Error** to Slack, email, or a database to get alerted on failed KSeF submissions.",
"height": 720,
"width": 700,
"color": 1
},
"id": "sticky-overview",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-760,
-220
]
},
{
"parameters": {
"content": "## Receive & Verify\nAccepts the Sellf webhook and checks its HMAC signature before trusting the payload.",
"height": 510,
"width": 1120,
"color": 7
},
"id": "sticky-section-a",
"name": "Section: Receive & Verify",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-760,
520
]
},
{
"parameters": {
"content": "## Acknowledge & Extract\nResponds immediately, then filters for completed purchases that requested an invoice with a NIP.",
"height": 350,
"width": 820,
"color": 7
},
"id": "sticky-section-b",
"name": "Section: Acknowledge & Extract",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
440,
520
]
},
{
"parameters": {
"content": "## Build the Invoice\nAssembles a KSeF-ready invoice payload from your seller details and the order.",
"height": 350,
"width": 520,
"color": 7
},
"id": "sticky-section-c",
"name": "Section: Build the Invoice",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1340,
520
]
},
{
"parameters": {
"content": "## Send & Log Result\nSends the invoice to your KSeF Gateway and records the resulting KSeF number, or the error if it fails.",
"height": 670,
"width": 820,
"color": 7
},
"id": "sticky-section-d",
"name": "Section: Send & Log Result",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1940,
360
]
},
{
"parameters": {
"content": "## Node setting required\nThis Code node needs `require('crypto')`. Self-hosted n8n: set `NODE_FUNCTION_ALLOW_BUILTIN=crypto` in the n8n environment, then restart.",
"height": 220,
"width": 300,
"color": 3
},
"id": "sticky-warning",
"name": "Warning: crypto module",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-130,
870
]
},
{
"parameters": {
"path": "sellf-purchase-ksef",
"httpMethod": "POST",
"responseMode": "responseNode",
"options": {
"rawBody": true
}
},
"id": "wh-1",
"name": "Sellf Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-700,
710
],
"notes": "Configure this webhook URL in Sellf admin panel \u2192 Settings \u2192 Webhooks. Raw Body is required so the signature check below sees the exact bytes Sellf signed.",
"onError": "continueRegularOutput"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a1",
"name": "ksefGatewayUrl",
"value": "https://your-ksef-gateway.example.com",
"type": "string"
},
{
"id": "a2",
"name": "sellerNip",
"value": "0000000000",
"type": "string"
},
{
"id": "a3",
"name": "sellerName",
"value": "Your Company sp. z o.o.",
"type": "string"
},
{
"id": "a4",
"name": "sellerStreet",
"value": "ul. Example 1",
"type": "string"
},
{
"id": "a5",
"name": "sellerCity",
"value": "00-001 Warszawa",
"type": "string"
},
{
"id": "a6",
"name": "defaultVatRate",
"value": 23,
"type": "number"
},
{
"id": "a7",
"name": "webhookSecret",
"value": "change-me-webhook-secret",
"type": "string"
},
{
"id": "a8",
"name": "gatewayApiKey",
"value": "your-gateway-api-key",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {
"stripBinary": false
}
},
"id": "config-1",
"name": "Configuration (EDIT ME)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-400,
710
],
"notes": "Fill in your KSeF Gateway URL, seller details, and webhook signing secret before activating this workflow."
},
{
"parameters": {
"jsCode": "const crypto = require('crypto');\nconst config = $('Configuration (EDIT ME)').first().json;\nconst item = $input.first();\n\nconst header = (item.json.headers && item.json.headers['x-sellf-signature']) || '';\nconst rawBody = item.binary && item.binary.data\n ? Buffer.from(item.binary.data.data, 'base64').toString('utf8')\n : JSON.stringify(item.json.body || {});\n\nlet t = null;\nlet v1 = null;\nfor (const part of header.split(',')) {\n const eq = part.indexOf('=');\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim();\n const value = part.slice(eq + 1).trim();\n if (key === 't') t = value;\n else if (key === 'v1') v1 = value;\n}\n\nlet valid = false;\nif (t && v1 && config.webhookSecret) {\n const expected = crypto.createHmac('sha256', config.webhookSecret).update(`${t}.${rawBody}`).digest('hex');\n const withinTolerance = Math.abs(Math.floor(Date.now() / 1000) - Number(t)) <= 300;\n const sameLength = expected.length === v1.length;\n valid = withinTolerance && sameLength && crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));\n}\n\nreturn [{ json: { valid, body: JSON.parse(rawBody) } }];"
},
"id": "code-verify",
"name": "Verify Signature",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-100,
710
],
"notes": "Verifies Sellf's t=...,v1=... HMAC-SHA256 header (see Sellf docs: webhook-queue/signature.ts) and re-parses the raw body for downstream nodes."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
]
},
"options": {}
},
"id": "if-sig",
"name": "Signature Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
200,
710
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{\"success\":false,\"error\":\"invalid signature\"}",
"options": {
"responseCode": 401
}
},
"id": "res-unauth",
"name": "Respond Unauthorized",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
200,
870
],
"notes": "Dead end: bad or missing signature. Nothing downstream trusts this payload."
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{\"status\":\"ok\"}"
},
"id": "res-1",
"name": "Respond OK",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
500,
710
],
"notes": "Responds immediately; the rest of the workflow keeps running afterwards to build and send the invoice."
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"leftValue": "={{ $json.body.event }}",
"rightValue": "purchase.completed",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "purchase.completed"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "sw-1",
"name": "Is Purchase?",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
800,
710
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json.body.data;\n\nreturn [{\n json: {\n email: data.customer.email,\n firstName: data.customer.firstName || '',\n productName: data.product.name,\n amount: data.order.amount / 100,\n currency: data.order.currency.toUpperCase(),\n sessionId: data.order.sessionId,\n paymentIntentId: data.order.paymentIntentId,\n needsInvoice: data.invoice?.needsInvoice || false,\n invoiceNip: data.invoice?.nip || null,\n invoiceCompany: data.invoice?.companyName || null,\n invoiceAddress: data.invoice?.address || null,\n invoiceCity: data.invoice?.city || null,\n invoicePostalCode: data.invoice?.postalCode || null,\n invoiceCountry: data.invoice?.country || 'PL'\n }\n}];"
},
"id": "code-1",
"name": "Extract Purchase Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
710
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"leftValue": "={{ $json.needsInvoice }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
},
{
"leftValue": "={{ $json.invoiceNip }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
]
},
"renameOutput": true,
"outputKey": "Needs Invoice"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "sw-2",
"name": "Needs Invoice?",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1400,
710
],
"notes": "Proceeds only when customer requested an invoice AND provided NIP. Both conditions required."
},
{
"parameters": {
"jsCode": "const d = $input.first().json;\nconst config = $('Configuration (EDIT ME)').first().json;\nconst today = new Date().toISOString().split('T')[0];\n\n// Build invoice number from session ID suffix - adjust this scheme if you have your own\nconst suffix = (d.sessionId || 'XXXX').slice(-4).toUpperCase();\nconst invoiceNumber = `FV/${today.replace(/-/g, '/')}/${suffix}`;\n\nconst buyerCity = d.invoicePostalCode\n ? `${d.invoicePostalCode} ${d.invoiceCity}`\n : d.invoiceCity || '-';\n\nconst seller = {\n nip: config.sellerNip,\n name: config.sellerName,\n address: {\n street: config.sellerStreet,\n city: config.sellerCity\n }\n};\n\nreturn [{\n json: {\n invoiceNumber,\n issueDate: today,\n saleDate: today,\n issuePlace: seller.address.city.split(' ').slice(1).join(' ') || 'Warszawa',\n currency: d.currency || 'PLN',\n type: 'VAT',\n seller,\n buyer: {\n nip: d.invoiceNip,\n name: d.invoiceCompany || d.email,\n address: {\n street: d.invoiceAddress || '-',\n city: buyerCity,\n country: d.invoiceCountry || 'PL'\n }\n },\n items: [\n {\n name: d.productName,\n unit: 'szt.',\n quantity: 1,\n unitPrice: d.amount,\n vatRate: config.defaultVatRate\n }\n ],\n payment: {\n paid: true,\n date: today,\n method: 'transfer'\n },\n // Pass-through for error logging\n _meta: {\n sessionId: d.sessionId,\n paymentIntentId: d.paymentIntentId,\n buyerEmail: d.email\n }\n }\n}];"
},
"id": "code-2",
"name": "Build KSeF Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1700,
710
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Configuration (EDIT ME)').item.json.ksefGatewayUrl }}/ksef/invoice",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "X-Api-Key",
"value": "={{ $('Configuration (EDIT ME)').item.json.gatewayApiKey }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ invoiceNumber: $json.invoiceNumber, issueDate: $json.issueDate, saleDate: $json.saleDate, issuePlace: $json.issuePlace, currency: $json.currency, type: $json.type, seller: $json.seller, buyer: $json.buyer, items: $json.items, payment: $json.payment }) }}",
"options": {
"timeout": 120000
}
},
"id": "http-1",
"name": "Send to KSeF Gateway",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2000,
710
],
"notes": "The gateway authenticates with KSeF internally - no auth headers needed here."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"leftValue": "={{ $json.success }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
]
},
"options": {}
},
"id": "if-1",
"name": "Success?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2300,
710
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "ksefNumber",
"name": "ksefNumber",
"value": "={{ $json.data.ksefNumber }}",
"type": "string"
},
{
"id": "status",
"name": "status",
"value": "={{ $json.data.status }}",
"type": "string"
},
{
"id": "sessionId",
"name": "sessionId",
"value": "={{ $('Build KSeF Payload').item.json._meta.sessionId }}",
"type": "string"
},
{
"id": "buyerEmail",
"name": "buyerEmail",
"value": "={{ $('Build KSeF Payload').item.json._meta.buyerEmail }}",
"type": "string"
}
]
},
"options": {}
},
"id": "set-1",
"name": "Extract KSeF Result",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2600,
550
],
"notes": "ksefNumber - save this to your DB (Supabase payment_transactions) for audit trail.\nOptionally chain a Supabase node here to persist it."
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "error",
"name": "error",
"value": "={{ $json.error || JSON.stringify($json) }}",
"type": "string"
},
{
"id": "sessionId",
"name": "sessionId",
"value": "={{ $('Build KSeF Payload').item.json._meta.sessionId }}",
"type": "string"
},
{
"id": "invoiceNumber",
"name": "invoiceNumber",
"value": "={{ $('Build KSeF Payload').item.json.invoiceNumber }}",
"type": "string"
}
]
},
"options": {}
},
"id": "set-2",
"name": "Log Error",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2600,
870
],
"notes": "TODO: Connect to Slack / email / Supabase audit_log to alert on failed KSeF submissions."
}
],
"connections": {
"Sellf Webhook": {
"main": [
[
{
"node": "Configuration (EDIT ME)",
"type": "main",
"index": 0
}
]
]
},
"Configuration (EDIT ME)": {
"main": [
[
{
"node": "Verify Signature",
"type": "main",
"index": 0
}
]
]
},
"Verify Signature": {
"main": [
[
{
"node": "Signature Valid?",
"type": "main",
"index": 0
}
]
]
},
"Signature Valid?": {
"main": [
[
{
"node": "Respond OK",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Unauthorized",
"type": "main",
"index": 0
}
]
]
},
"Respond OK": {
"main": [
[
{
"node": "Is Purchase?",
"type": "main",
"index": 0
}
]
]
},
"Is Purchase?": {
"main": [
[
{
"node": "Extract Purchase Data",
"type": "main",
"index": 0
}
]
]
},
"Extract Purchase Data": {
"main": [
[
{
"node": "Needs Invoice?",
"type": "main",
"index": 0
}
]
]
},
"Needs Invoice?": {
"main": [
[
{
"node": "Build KSeF Payload",
"type": "main",
"index": 0
}
]
]
},
"Build KSeF Payload": {
"main": [
[
{
"node": "Send to KSeF Gateway",
"type": "main",
"index": 0
}
]
]
},
"Send to KSeF Gateway": {
"main": [
[
{
"node": "Success?",
"type": "main",
"index": 0
}
]
]
},
"Success?": {
"main": [
[
{
"node": "Extract KSeF Result",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Error",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"meta": {
"templateCredsSetupCompleted": true
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Sellf: Purchase → KSeF Invoice. Uses httpRequest. Webhook trigger; 20 nodes.
Source: https://github.com/jurczykpawel/ksef-gateway/blob/45af8720814454ef53dff12543891ac72733a0d4/examples/n8n/sellf-ksef.json — 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.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.
This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c