AutomationFlowsWeb Scraping › Sellf: Purchase → Ksef Invoice

Sellf: Purchase → Ksef Invoice

Sellf: Purchase → KSeF Invoice. Uses httpRequest. Webhook trigger; 20 nodes.

Webhook trigger★★★★☆ complexity20 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 20 Complexity: ★★★★☆ Added:

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
{
  "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
  }
}
Pro

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 →

More Web Scraping workflows → · Browse all categories →

Related workflows

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

Web Scraping

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

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.

HTTP Request
Web Scraping

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

HTTP Request
Web Scraping

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

HTTP Request